> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aveid.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Encryption model and key lifecycle

> How Ave delivers per-app, per-identity encryption keys — and how to handle them safely.

## How it works

Ave's E2EE model gives each (app, identity) pair a **stable, isolated encryption context**. The server stores an *encrypted* form of your app key, protected by the user's master key during the first authorization. From then on, Ave delivers the key back to you on the post-authorization redirect via URL fragment.

At consent time, the Ave UI decrypts that ciphertext **in the browser** using the user's master key, then places the plaintext app key in the fragment. The token and authorization-code responses do not carry the decrypted secret.

When a passkey supports PRF, Ave uses its output in the browser to encrypt and unlock the user's master key. The PRF output is omitted from credentials sent to the API for registration, sign-in, and key unlocking. Only the encrypted master key is stored on the server.

Saved browser master keys are associated with the account's authenticated identities. Sign-in prefers the key unlocked by the verified passkey; a key from another account cannot substitute for it. Previously saved keys without an account association are reused only after they successfully decrypt an existing key for the current identity. Unmatched keys remain available for recovery. Passkey and recovery-code responses identify the account that owns the recovered key; Ave checks that association before saving it. Device approval applies the session returned with the transferred key. When persistent browser storage is unavailable, an unlocked key remains available for the current page session.

For **multi-user asymmetric** encryption, enable `e2ee:asymmetric` on your app and use per-user app keypairs with the [lookup API](/sdk/sdk-identity-keys). The old identity wrapped OAuth payload flow (`wrapped_key`) is removed.

| Property     | Symmetric (`e2ee:symmetric`)                 | Asymmetric (`e2ee:asymmetric`)                     |
| ------------ | -------------------------------------------- | -------------------------------------------------- |
| Key material | AES-256 app key                              | ECDH P-256 public + private keypair                |
| Fragment     | `#app_key=`                                  | `#app_public_key=` + `#app_private_key=`           |
| Multi-user   | Encrypt with shared app key or roll your own | Lookup public keys; encrypt without Ave handshakes |

## Key lifecycle

<Steps>
  <Step title="First authorization (key provisioning)">
    When a user authorizes your E2EE-capable app for the first time, the app generates key material client-side and encrypts it with the user's passkey. The encrypted key is sent along with the authorization request.
  </Step>

  <Step title="Grant storage">
    Ave stores the encrypted key context in the user's authorization record. The server never holds the plaintext.
  </Step>

  <Step title="Key handoff via URL fragment">
    After the user consents, the Ave authorization UI reads the server-stored encrypted key and decrypts it on the client side using the user's master key. It then passes the plaintext app key to your callback as a URL fragment (`#app_key=...`). The server value is **never** sent — the fragment contains the already-decrypted key. Parse it from the fragment before or alongside your authorization code exchange.

    ```ts theme={null}
    // Parse the app key from the URL fragment before clearing it
    const hashParams = new URLSearchParams(window.location.hash.slice(1));
    const rawKey = hashParams.get("app_key");

    // Clear the fragment from browser history immediately
    window.history.replaceState({}, "", window.location.pathname + window.location.search);
    ```
  </Step>

  <Step title="Key import">
    Your app normalizes the base64 key payload (URL fragments can turn `+` into a space), decodes it, and imports it into the Web Crypto API.

    ```ts theme={null}
    // Normalize base64 — URL fragments turn + into space
    const normalized = rawKey.replace(/ /g, "+");
    const keyBytes = Uint8Array.from(atob(normalized), c => c.charCodeAt(0));

    const cryptoKey = await crypto.subtle.importKey(
      "raw",
      keyBytes,
      { name: "AES-GCM" },
      false,
      ["encrypt", "decrypt"]
    );
    ```
  </Step>

  <Step title="Encrypt and decrypt data">
    Use AES-GCM with a unique IV for every encryption operation.

    ```ts theme={null}
    // Encrypt
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const ciphertext = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv },
      cryptoKey,
      plaintext
    );

    // Decrypt
    const plaintext = await crypto.subtle.decrypt(
      { name: "AES-GCM", iv },
      cryptoKey,
      ciphertext
    );
    ```
  </Step>

  <Step title="Identity switch handling">
    When the user switches identities, transition the encryption context. Different identities have different keys.

    Key storage pattern: `app:{appId}:identity:{identityId}:data`

    Never merge or share encrypted datasets across identities.
  </Step>
</Steps>

## Fragment hygiene

The app key arrives as a URL fragment (`#app_key=...`). Browsers silently replace `+` with a space in fragment parameters, which breaks base64 decoding.

```ts theme={null}
// Always normalize before atob():
const safe = rawKey.replace(/ /g, "+");
```

After parsing the key, remove the fragment from the browser's history to prevent it from appearing in logs or being shared via the back button.

## Failure handling

<AccordionGroup>
  <Accordion title="Key missing for an E2EE-required route">
    Block the action and prompt the user to re-authorize your app. Do not silently degrade to unencrypted storage.
  </Accordion>

  <Accordion title="Decryption fails">
    Treat this as a key-context mismatch. Do not overwrite existing ciphertext — you could destroy data. Prompt re-authentication and re-grant.
  </Accordion>

  <Accordion title="Corrupted payload">
    Fail closed. Request user re-authentication rather than attempting to recover from a potentially tampered key payload.
  </Accordion>
</AccordionGroup>
