Skip to main content
Ave implements end-to-end encryption (E2EE) to ensure that only you can read your data. The server stores encrypted blobs that it cannot decrypt, providing a zero-knowledge architecture.

Encryption Architecture

The master key never leaves your device in plaintext. All encryption and decryption happens client-side in your browser.

Master Key Generation

During registration, your browser generates a 256-bit AES-GCM master key:
// Generate master key using Web Crypto API
export async function generateMasterKey(): Promise<CryptoKey> {
  return await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true, // extractable (for backup)
    ["encrypt", "decrypt"]
  );
}
See ave-web/src/lib/crypto.ts:19.
1

Key Generation

Master key is created using crypto.subtle.generateKey() with cryptographically secure randomness.
2

Local Storage

Master key is stored in browser localStorage (base64 encoded) for reuse.
export async function storeMasterKey(masterKey: CryptoKey): Promise<void> {
  const keyData = await exportMasterKey(masterKey);
  const encoded = btoa(String.fromCharCode(...new Uint8Array(keyData)));
  localStorage.setItem("ave_master_key", encoded);
}
3

Backup Creation

Master key is encrypted with trust codes and uploaded to server.

Data Encryption

All sensitive data is encrypted before sending to the server:
// Encrypt data with AES-GCM
export async function encrypt(
  data: string | ArrayBuffer,
  key: CryptoKey
): Promise<string> {
  const encoder = new TextEncoder();
  const dataBuffer = typeof data === "string" 
    ? encoder.encode(data) 
    : data;
  
  // Generate random 12-byte IV (nonce)
  const iv = crypto.getRandomValues(new Uint8Array(12));
  
  // Encrypt data
  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    dataBuffer
  );
  
  // Combine IV + ciphertext and encode as base64
  const combined = new Uint8Array(iv.length + encrypted.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(encrypted), iv.length);
  
  return btoa(String.fromCharCode(...combined));
}
See ave-web/src/lib/crypto.ts:85.

Encryption Format

Encrypted data is stored as base64 in this format:
[12-byte IV][variable-length ciphertext][16-byte auth tag]
  • IV (Initialization Vector): Random 12-byte nonce, unique per encryption
  • Ciphertext: Encrypted data
  • Auth Tag: 16-byte GCM authentication tag (prevents tampering)
Never reuse IVs! Each encryption operation generates a fresh random IV. Reusing IVs with AES-GCM is catastrophic for security.

Data Decryption

Decryption reverses the process:
export async function decrypt(
  encryptedData: string,
  key: CryptoKey
): Promise<ArrayBuffer> {
  // Decode base64
  const combined = Uint8Array.from(
    atob(encryptedData), 
    (c) => c.charCodeAt(0)
  );
  
  // Extract IV and ciphertext
  const iv = combined.slice(0, 12);
  const ciphertext = combined.slice(12);
  
  // Decrypt
  return await crypto.subtle.decrypt(
    { name: "AES-GCM", iv },
    key,
    ciphertext
  );
}
See ave-web/src/lib/crypto.ts:112.

Master Key Backup

To enable account recovery, the master key is encrypted with trust codes and backed up to the server:
export async function createMasterKeyBackup(
  masterKey: CryptoKey,
  trustCodes: string[]
): Promise<string> {
  // Export master key to raw bytes
  const masterKeyData = await exportMasterKey(masterKey);
  
  // Encrypt with each trust code
  const backups: string[] = [];
  
  for (const code of trustCodes) {
    // Derive encryption key from trust code using PBKDF2
    const derivedKey = await deriveKeyFromTrustCode(code);
    
    // Encrypt master key
    const encrypted = await encrypt(masterKeyData, derivedKey);
    backups.push(encrypted);
  }
  
  // Store as JSON
  return JSON.stringify({
    version: 1,
    backups
  });
}
See ave-web/src/lib/crypto.ts:144.

Trust Code Key Derivation

Trust codes are converted to encryption keys using PBKDF2:
export async function deriveKeyFromTrustCode(code: string): Promise<CryptoKey> {
  // Normalize: uppercase, strip non-alphanumeric
  const normalized = code.toUpperCase().replace(/[^A-Z0-9]/g, "");
  const encoder = new TextEncoder();
  
  // Import as key material
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    encoder.encode(normalized),
    "PBKDF2",
    false,
    ["deriveKey"]
  );
  
  // Derive AES key using PBKDF2
  const salt = encoder.encode("ave-trust-code-salt-v1");
  
  return await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt,
      iterations: 100000, // 100k iterations
      hash: "SHA-256"
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );
}
See ave-web/src/lib/crypto.ts:51.
100,000 PBKDF2 iterations provide reasonable protection against brute-force attacks while remaining fast enough for user experience. In production, consider increasing to 600,000+ iterations.

Recovery Flow

When recovering your account with a trust code:
1

Enter Trust Code

User provides one of their saved trust codes.
2

Fetch Encrypted Backup

Client retrieves encryptedMasterKeyBackup from server.
POST /api/login/trust-code
{
  "handle": "alice",
  "code": "ABCDE-12345-FGHIJ-67890-KLMNO",
  "device": { ... }
}

Response:
{
  "sessionToken": "...",
  "encryptedMasterKeyBackup": "{\"version\":1,\"backups\":[...]}" 
}
3

Derive Decryption Key

Trust code is normalized and converted to encryption key via PBKDF2.
4

Decrypt Master Key

Each backup in the JSON is tried until one successfully decrypts.
export async function recoverMasterKeyFromBackup(
  backup: string,
  trustCode: string
): Promise<CryptoKey | null> {
  const data = JSON.parse(backup);
  const derivedKey = await deriveKeyFromTrustCode(trustCode);
  
  // Try each backup
  for (const encryptedBackup of data.backups) {
    try {
      const masterKeyData = await decrypt(encryptedBackup, derivedKey);
      return await importMasterKey(masterKeyData);
    } catch {
      continue; // Wrong trust code, try next
    }
  }
  
  return null;
}
5

Store Master Key

Recovered master key is stored in localStorage for future use.
See ave-web/src/lib/crypto.ts:171.

Multi-Device Key Transfer

When logging in on a new device via device approval, the master key is transferred using ephemeral ECDH (Elliptic Curve Diffie-Hellman) key exchange:

ECDH Key Exchange

1

Device 2: Generate Ephemeral Keypair

The requesting device generates a one-time ECDH P-256 keypair:
const keyPair = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  true,
  ["deriveKey"]
);

// Export public key to send to server
const publicKeyData = await crypto.subtle.exportKey(
  "spki", 
  keyPair.publicKey
);
const publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKeyData)));
2

Device 1: Encrypt Master Key

The approving device:
  1. Generates its own ephemeral keypair
  2. Imports Device 2’s public key
  3. Derives shared secret via ECDH
  4. Encrypts master key with shared secret
// Import requester's public key
const recipientPublicKey = await importPublicKey(requesterPublicKeyB64);

// Derive shared secret
const sharedKey = await crypto.subtle.deriveKey(
  { name: "ECDH", public: recipientPublicKey },
  senderPrivateKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

// Encrypt master key
const encrypted = await encrypt(masterKeyData, sharedKey);
3

Device 2: Decrypt Master Key

The requesting device:
  1. Imports Device 1’s public key
  2. Derives same shared secret using its private key
  3. Decrypts master key
const senderPublicKey = await importPublicKey(approverPublicKeyB64);
const sharedKey = await deriveSharedKey(recipientPrivateKey, senderPublicKey);
const masterKeyData = await decrypt(encryptedMasterKey, sharedKey);
const masterKey = await importMasterKey(masterKeyData);
See ave-web/src/lib/crypto.ts:256 for implementation.
Ephemeral keys are never stored - they’re generated on-demand for each login request and discarded after use. This provides forward secrecy: even if a key is compromised, past transfers remain secure.

PRF-Based Encryption

Passkeys supporting the PRF (Pseudo-Random Function) extension can derive deterministic secrets. Ave uses this to store a master key backup that only unlocks with the specific passkey:
// During passkey registration
const credential = await navigator.credentials.create({
  publicKey: {
    ...options,
    extensions: {
      prf: { eval: { first: salt } } // Request PRF output
    }
  }
});

const prfOutput = credential.getClientExtensionResults().prf?.results.first;

if (prfOutput) {
  // Encrypt master key with PRF output
  const prfEncrypted = await encryptMasterKeyWithPrf(masterKey, prfOutput);
  
  // Store with passkey
  await updatePasskey({ prfEncryptedMasterKey: prfEncrypted });
}
During login, the same PRF output decrypts the master key:
const prfOutput = credential.getClientExtensionResults().prf?.results.first;
const masterKey = await decryptMasterKeyWithPrf(
  passkey.prfEncryptedMasterKey,
  prfOutput
);
See ave-web/src/lib/crypto.ts:342.
PRF support varies by platform. Always provide fallback recovery methods (trust codes) for users whose authenticators don’t support PRF.

OAuth App Encryption

When authorizing OAuth apps that support E2EE, each app gets its own encryption key:
// Generate app-specific key
const appKey = await generateAppKey();

// Encrypt app key with user's master key
const encryptedAppKey = await encryptAppKey(appKey, masterKey);

// Store on server
await db.insert(oauthAuthorizations).values({
  userId: user.id,
  appId: app.id,
  identityId: identity.id,
  encryptedAppKey // Only decryptable by user
});

// Send app key to OAuth app (over secure channel)
return { appKey: await exportAppKey(appKey) };
This ensures:
  • Apps cannot access each other’s encryption keys
  • Users retain control over app data encryption
  • Revoking app access also revokes encryption keys
See ave-web/src/lib/crypto.ts:366.

Security Properties

Confidentiality

Server cannot decrypt data - Server has encrypted blobs without keys Transit encryption - HTTPS protects data in transit At-rest encryption - Data encrypted before upload

Integrity

AES-GCM authentication - 16-byte auth tag prevents tampering Passkey signatures - WebAuthn prevents credential forgery

Availability

Multiple recovery methods - Trust codes, device approval, PRF Multi-device support - Master key can be transferred securely ⚠️ Trust code requirement - Losing all trust codes and devices means data loss

Cryptographic Primitives

OperationAlgorithmParameters
Symmetric EncryptionAES-GCM256-bit keys, 12-byte IV, 16-byte tag
Key DerivationPBKDF2SHA-256, 100k iterations, static salt
Key ExchangeECDHP-256 curve (secp256r1)
HashingSHA-256Server-side for tokens/codes
Random Generationcrypto.getRandomValues()Browser CSPRNG
All cryptographic operations use the Web Crypto API (crypto.subtle), which provides hardware-accelerated, constant-time implementations.

Best Practices

For Users

  1. Save trust codes immediately - They’re your backup if devices are lost
  2. Use multiple devices - Register passkeys on 2+ devices for redundancy
  3. Enable PRF passkeys - Modern authenticators provide seamless recovery
  4. Don’t share master keys - Never export or share your master key

For Developers

  1. Never log keys - Master keys should never appear in logs
  2. Use constant-time operations - Web Crypto API prevents timing attacks
  3. Validate ciphertext - GCM auth tag verification prevents tampering
  4. Generate fresh IVs - Never reuse IVs with the same key
  5. Secure deletion - Overwrite keys in memory when done (where possible)

Threat Model

Ave’s E2EE protects against: Compromised server - Server cannot decrypt stored data Network eavesdropping - HTTPS + E2EE provide defense in depth Malicious insiders - Database admins cannot read user data Data breaches - Stolen database remains encrypted ⚠️ Client compromise - Malware on user’s device can steal master key from memory/localStorage ⚠️ Browser vulnerabilities - Web Crypto API relies on browser security

Next Steps

Key Management

Master key lifecycle and recovery

WebAuthn Implementation

How passkeys protect master keys

Build docs developers (and LLMs) love