Encryption
Vault uses industry-standard encryption to protect your passwords. All cryptographic operations use the Web Crypto API.
Encryption Flow
Master Password
│
▼
┌──────────────┐
│ PBKDF2 │ ← Salt (random, stored on server)
│ (600,000 │
│ iterations) │
└──────────────┘
│
▼
Key Encryption Key (KEK)
│
▼
┌──────────────┐
│ AES-GCM │ ← Wraps/unwraps the Vault Key
│ Unwrap │
└──────────────┘
│
▼
Vault Key (random 256-bit)
│
▼
┌──────────────┐
│ AES-GCM │ ← Encrypts/decrypts vault entries
│ Decrypt │
└──────────────┘
│
▼
Plaintext EntriesKey Derivation (PBKDF2)
Your master password is converted to a Key Encryption Key (KEK) using PBKDF2:
const kek = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt, // 16 bytes, random, stored on server
iterations: 600000, // OWASP recommended minimum
hash: "SHA-256"
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["wrapKey", "unwrapKey"]
);Why PBKDF2?
- Browser-native: Web Crypto API support
- Configurable iterations: Can increase over time
- Proven security: Well-studied algorithm
- Memory-hard alternative: Argon2 considered for future
Iteration Count
| Year | OWASP Recommendation |
|---|---|
| 2023 | 600,000 |
| Future | Increases with hardware |
Vault Key
Each vault has a unique 256-bit key generated randomly:
const vaultKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // Extractable for wrapping
["encrypt", "decrypt"]
);Key Wrapping
The vault key is wrapped (encrypted) with the KEK before storage:
// Wrap vault key for storage
const wrappedKey = await crypto.subtle.wrapKey(
"raw",
vaultKey,
kek,
{ name: "AES-GCM", iv: iv }
);
// Unwrap vault key for use
const vaultKey = await crypto.subtle.unwrapKey(
"raw",
wrappedKey,
kek,
{ name: "AES-GCM", iv: iv },
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);Vault Encryption (AES-GCM)
Vault entries are encrypted with AES-256-GCM:
// Encrypt
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
vaultKey,
encodedData
);
// Decrypt
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
vaultKey,
encryptedData
);Why AES-GCM?
- Authenticated: Built-in integrity check
- Fast: Hardware acceleration (AES-NI)
- Secure: No known practical attacks
- Standard: NIST approved, widely used
Data Format
Encrypted Vault (stored on server)
{
encryptedData: string, // Base64(AES-GCM(JSON(entries)))
iv: string, // Base64(12 random bytes)
wrappedVaultKey: string, // Base64(AES-GCM(vaultKey))
vaultKeyIv: string, // Base64(12 random bytes)
vaultKeySalt: string, // Base64(16 random bytes)
version: number
}Decrypted Entry (client only)
{
id: string,
type: "login" | "note" | "card" | "identity",
name: string,
username?: string,
password: string,
url?: string,
notes?: string,
tags: string[],
favorite: boolean,
createdAt: string,
updatedAt: string
}IV (Initialization Vector)
Every encryption operation uses a fresh random IV:
const iv = crypto.getRandomValues(new Uint8Array(12));Important: Never reuse an IV with the same key. Vault generates a new IV for every save operation.
Security Properties
Confidentiality
- 256-bit AES encryption
- Keys never leave client unencrypted
- Server cannot read vault contents
Integrity
- GCM mode provides authentication
- Tampering detected on decryption
- Version field prevents replay attacks
Forward Secrecy
- Changing master password re-encrypts vault key
- Old wrapped keys cannot decrypt new data
- Compromised KEK only affects current wrapping
Code Reference
Encryption utilities are in @pwm/shared:
import {
// Key derivation
deriveKeyFromPassword,
// Vault key operations
generateVaultKey,
wrapVaultKey,
unwrapVaultKey,
// Vault encryption
encryptWithVaultKey,
decryptWithVaultKey
} from "@pwm/shared";Related
- Zero-Knowledge Security - Security overview
- Passkeys - Authentication security
- Vaults API - How encrypted data is stored