Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 Entries

Key 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

YearOWASP Recommendation
2023600,000
FutureIncreases 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