E2EE Architecture
Key Derivation and Protection
Password to KEK derivation and unlock flows
Key Derivation and Protection
Password to Key Encryption Key (KEK)
When a user sets up their account or unlocks with a password:
Process
- Generate random 128-bit salt (stored with encrypted key)
- Derive KEK:
KEK = Argon2id(password, salt, memory=256MB, ops=3) - Encrypt master key:
encrypted = XSalsa20(masterKey, KEK) - Securely zero KEK from memory
- Store:
{salt, opsLimit, memLimit, encryptedMasterKey, nonce}
Stored Key Bundle
The storable key bundle contains all information needed to unlock:
interface StorableUserKeys {
// Password encryption parameters
kekSalt: string; // Base64 Argon2id salt
kekOpsLimit: number; // Argon2id iterations
kekMemLimit: number; // Argon2id memory (bytes)
// Encrypted keys
encryptedMasterKey: string; // XSalsa20(masterKey, KEK)
masterKeyNonce: string; // Nonce for above
encryptedPrivateKey: string; // XSalsa20(privateKey, masterKey)
privateKeyNonce: string; // Nonce for above
encryptedRecoveryKey: string; // XSalsa20(recoveryKey, masterKey)
recoveryKeyNonce: string; // Nonce for above
// Recovery mechanism
masterKeyRecovery: string; // XSalsa20(masterKey, recoveryKey)
masterKeyRecoveryNonce: string;
// Public information
publicKey: string; // Plaintext, for sharing
}Unlock Flow
Memory Safety
All sensitive key material is:
- Allocated using libsodium's secure memory functions
- Zeroed immediately after use with
sodium.memzero() - Never logged or serialized to disk in plaintext
- Cleared on session lock or page unload
// Example: Secure key handling
async function unlockWithPassword(password: string) {
const kek = await deriveKEK(password, salt, opsLimit, memLimit);
try {
const masterKey = await decrypt(encryptedMasterKey, kek, nonce);
await storeInSecureSession(masterKey);
} finally {
// Always zero sensitive material
sodium.memzero(kek);
}
}