Encryption architecture
The four-layer key hierarchy — PDS, KEK, UMK, and per-vault DEKs — plus the wrap relationships, supported algorithms, and the ciphertext envelope format.
PulsVault protects vault data with a four-layer key hierarchy. Keys wrap keys: your credentials derive a key that unwraps your master key, which unwraps per-vault keys, which encrypt individual item fields. Every layer lives and dies on the client.
The key hierarchy#
| Layer | Key | Generated | Stored | Wrapped with |
|---|---|---|---|---|
| 1 | PDS — Password-Derived Secret | At every login: PBKDF2-SHA256(password, SHA256(email), 600k iterations) | Client memory only — kept five minutes for the first unlock, then wiped | — |
| 1 | KEK — Key Encryption Key (256-bit) | At vault unlock, by combining the PDS with the Access Key (KDF ≥ 600k iterations) | Never — memory only, wiped after use | — |
| 2 | UMK — User Master Key (random 256-bit) | Once, at signup, on the client | users.encrypted_master_key |
KEK via AES-256-GCM |
| 3 | DEK — per-vault Data Encryption Key (random 256-bit) | Once, at vault creation, on the client | vaults.encrypted_dek |
UMK via AES-256-GCM |
| 4 | Ciphertext — encrypted item fields | At every create/update | Item columns (opaque strings) | DEK via the vault's algorithm |
Additional wraps of the UMK#
The UMK is the pivot of the hierarchy, so it carries independent wraps for different unlock paths — never the raw key:
- Primary wrap — AES-256-GCM under the KEK (password + Access Key path).
- Recovery wrap — under a KEK derived from the Recovery Key alone; used during account recovery to re-wrap the UMK for a new password without touching any vault data.
- Device wrap — RSA-OAEP-2048 to a hardware-backed key (Secure Enclave / Android Keystore) on a trusted mobile; see Trusted-device unlock.
Per-vault DEKs and algorithms#
Each vault has its own independently random DEK, so compromising one vault's key exposes nothing about the others. The encryption algorithm is resolved by plan and organization policy:
| Algorithm | Notes | Plans |
|---|---|---|
AES_256_GCM |
Authenticated, hardware-accelerated; the default | All plans |
CHACHA20_POLY1305 |
Constant-time, fast without AES-NI | Team+ |
XCHACHA20_POLY1305 |
192-bit nonce eliminates IV-collision risk | Enterprise |
NONE |
Explicit plaintext opt-out | Enterprise, organization-admin approval only |
The primary vault is always AES-256-GCM, hardcoded at signup, regardless of organization encryption policy — and it is never shareable. Signup does not complete until the client has generated and stored the primary vault's wrapped DEK.
Ciphertext envelope format#
Every encrypted field is a self-describing JSON envelope, Base64-encoded and stored as an opaque string:
{ "v": 2, "alg": "AES-GCM", "iv": "<base64 12-byte IV>", "data": "<base64 ciphertext>" }alg is AES-GCM, CHACHA20-POLY1305 or XCHACHA20-POLY1305. There is no separate tag field:
the authentication tag is appended to the ciphertext and travels inside data.
Each ciphertext is bound to what it encrypts through AEAD associated data:
field: pv-field-v2|<vault id>|<item id>|<field name>
history: pv-history-v2|<item id>|<field name>|<history id>The ids are random, immutable identifiers your app generates when it first encrypts a vault, item, note, folder or attachment — not database keys, so a restored backup still decrypts. A server that swapped two ciphertexts — your password into the username field, or one item's secret into another — gets a decryption failure, never the wrong secret. Only version 2 is written or read.
Decryption dispatches on the alg field, making stored data forward-compatible across
algorithms. Every AES-GCM encryption uses a unique 12-byte IV — IVs are never reused under
the same key. Only sensitive fields (passwords, tokens, PINs, private keys, seed phrases, card
numbers, …) are encrypted; an item's name, type, tags, remarks and non-secret fields are stored
readable by the server (what is and isn't encrypted).
Key lifecycle properties#
- Password change re-wraps, never re-encrypts. A new KEK re-wraps the UMK — one database field changes; all DEKs and ciphertext are untouched.
- Memory-only UMK. The unwrapped UMK and the vault DEKs live in client memory only, with a user-chosen session length (5 minutes to never-expire). They are wiped on expiry, lock or logout — and a page reload starts locked, because nothing that can decrypt is ever written to browser storage. Locking one tab locks every open tab.
- PDS alone is useless. It must be combined with the Access Key to derive the KEK.
- RSA is never used for bulk data — only to wrap symmetric keys (sharing, device wraps).
Related#
- Zero-knowledge model
- Vault sharing & DEK re-wrapping
- How encryption works — the user-level explanation