Architecture
The zero-trust vault ensures the server never has access to your plaintext data. All encryption and decryption happens in your browser using the Web Crypto API.Key derivation
| Parameter | Value |
|---|---|
| Algorithm | Argon2id |
| Memory | 64 MiB (fallback: 32 MiB) |
| Iterations | 3 (fallback: 4) |
| Parallelism | 4 lanes |
| Output | 256-bit AES key |
| Salt | Per-user random, stored in vault_salts table |
The low-memory fallback (32 MiB, 4 iterations) activates automatically on devices with constrained memory. It maintains equivalent security with a slightly different parameter set.
Salt management
- Generated client-side using
crypto.getRandomValues() - Stored server-side in the
vault_user_salttable — one salt per user - Created once during vault setup (POST
/api/vault/salt) - Public by design — the salt ensures different users derive different keys from the same password, but is not secret
Encryption flow
Derive key
The vault key is derived from your master password + salt using Argon2id. The result is imported as a
CryptoKey via crypto.subtle.importKey().Generate IV
A random 12-byte initialization vector is generated for each encryption operation using
crypto.getRandomValues().Encrypt
The plaintext is encrypted using
crypto.subtle.encrypt() with AES-256-GCM, producing ciphertext and an authentication tag.TOTP secrets
TOTP secrets use the same zero-trust model:- Encrypted with the vault key before storage
- Stored as
zerotrust:{ iv, ciphertext }in the database - TOTP codes are generated client-side using
HMAC-SHA1 - The server returns
410 Goneif you request a code for a zero-trust entry
Security properties
- Forward secrecy: changing your master password re-encrypts all vault data with a new key
- No key escrow: PassAgent cannot decrypt your vault under any circumstances
- Memory safety: vault key is held only in JavaScript memory (React state) and wiped on lock/logout
- Brute-force resistance: Argon2id’s memory-hard properties make offline attacks expensive