Skip to main content

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 — it’s encrypted with the user’s passkey 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. For asymmetric flows (encrypting an invite or secret for another user’s identity), use identity keypairs and wrapped payloads in the JavaScript SDK.
PropertyValue
Key isolationPer-app, per-identity — different identities get different keys
Key stabilitySame app + same identity always gets the same key context
Server knowledgeZero — Ave stores only the encrypted key, never plaintext
Encryption algorithmAES-GCM with a unique IV per ciphertext

Key lifecycle

1

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.
2

Grant storage

Ave stores the encrypted key context in the user’s authorization record. The server never holds the plaintext.
3

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.
// 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);
4

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.
// 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"]
);
5

Encrypt and decrypt data

Use AES-GCM with a unique IV for every encryption operation.
// 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
);
6

Identity switch handling

When the user switches identities, transition the encryption context. Different identities have different keys.Key storage pattern: app:{appId}:identity:{identityId}:dataNever merge or share encrypted datasets across identities.

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.
// 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

Block the action and prompt the user to re-authorize your app. Do not silently degrade to unencrypted storage.
Treat this as a key-context mismatch. Do not overwrite existing ciphertext — you could destroy data. Prompt re-authentication and re-grant.
Fail closed. Request user re-authentication rather than attempting to recover from a potentially tampered key payload.
Last modified on April 13, 2026