Key Hierarchy
SyVault uses a 4-layer key hierarchy to separate authentication from encryption, isolate vaults from each other, and limit the blast radius of any single key compromise. Every layer uses domain separation to ensure that keys derived for one purpose cannot be repurposed for another.
Hierarchy Diagram
Master Password
│
▼
┌─────────────────────────────────────────────┐
│ Argon2id (64 MiB, 3 iterations, 4 lanes) │
│ salt: random 16 bytes per account │
└─────────────────────────────────────────────┘
│
▼
Master Key (256-bit)
│
├──► HKDF-SHA256 (info: "syvault.auth.v1:<email>")
│ └──► Auth Hash ──► sent to server ──► server stores Argon2id(auth_hash)
│
└──► HKDF-SHA256 (info: "syvault.enc.v1")
└──► Encryption Key (NEVER sent to server)
│
▼
┌───────────────────────┐
│ AES-256-GCM unwrap │
│ AAD: "syvault. │
│ account-key.v1" │
└───────────────────────┘
│
▼
Account Key (256-bit, random)
│
├──► HKDF-SHA256 (info: "syvault.vault.{vault_id}.v1")
│ └──► Vault Key A
│
├──► HKDF-SHA256 (info: "syvault.vault.{vault_id}.v1")
│ └──► Vault Key B
│
└──► ... (one Vault Key per vault)
│
▼
┌───────────────────────────────┐
│ AES-256-GCM wrap/unwrap │
│ AAD: "syvault.record. │
│ {record_id}.dek.v1" │
└───────────────────────────────┘
│
▼
Record DEK (256-bit, random)
│
▼
┌───────────────────────────────┐
│ AES-256-GCM encrypt/decrypt │
│ AAD: "syvault.record. │
│ {record_id}.payload.v1" │
└───────────────────────────────┘
│
▼
Record Plaintext
Layer 0: Master Key
The Master Key is derived from the user's master password and a random 16-byte salt using Argon2id with the following parameters:
| Parameter | Value |
|---|---|
| Memory | 64 MiB (65,536 KiB) |
| Iterations | 3 |
| Parallelism | 4 lanes |
| Output length | 32 bytes (256 bits) |
| Salt | 16 bytes, random per account, stored server-side |
Argon2id was selected because it is the recommended KDF per OWASP and the winner of the Password Hashing Competition. The 64 MiB memory parameter makes GPU/ASIC-based brute force attacks economically infeasible.
The Master Key is then domain-separated via HKDF-SHA256 into two independent keys:
- Auth Hash:
HKDF-SHA256(ikm=master_key, info="syvault.auth.v1:<email>", L=32)-- sent to the server for authentication. The server stores a second Argon2id hash of this value. - Encryption Key:
HKDF-SHA256(ikm=master_key, info="syvault.enc.v1", L=32)-- used to unwrap the Account Key. Never transmitted.
The Auth Hash and Encryption Key are cryptographically independent. Compromising the Auth Hash (e.g., from a server breach) does not reveal the Encryption Key or any vault data. This is the fundamental security property of SyVault's key derivation.
Layer 1: Account Key
The Account Key is a random 256-bit key generated once at account creation. Unlike the Master Key, it is not derived from a password -- it has full 256-bit entropy. The Account Key is wrapped (encrypted) by the Encryption Key using AES-256-GCM with AAD syvault.account-key.v1 and stored on the server.
Why a separate Account Key?
- Password changes do not re-encrypt the vault. When you change your master password, only the Account Key wrapper is re-encrypted. All vault keys and record DEKs remain unchanged.
- Multiple unlock providers. The Account Key can be wrapped by different keys: the password-derived Encryption Key, a device-stored key (for SSO users), or an organization recovery key (for admin-assisted recovery).
Layer 2: Vault Keys
Each vault gets its own Vault Key, derived from the Account Key via HKDF-SHA256:
Vault Key = HKDF-SHA256(ikm=account_key, info="syvault.vault.{vault_id}.v1", L=32)
Because HKDF is deterministic, the same Account Key and vault ID always produce the same Vault Key. There is no additional key material to store for vault keys -- they are derived on demand.
Layer 3: Record DEKs
Each record gets a unique, random 256-bit DEK. The DEK is wrapped by its parent Vault Key using AES-256-GCM:
Wrapped DEK = AES-256-GCM(key=vault_key, aad="syvault.record.{record_id}.dek.v1", plaintext=dek)
The record payload is then encrypted by the DEK:
Encrypted Payload = AES-256-GCM(key=dek, aad="syvault.record.{record_id}.payload.v1", plaintext=payload)
Both the wrapped DEK and the encrypted payload are stored on the server. The record ID is embedded in the AAD at both layers, ensuring that wrapped DEKs and encrypted payloads cannot be swapped between records.
The record_id is generated client-side (UUIDv4) before encryption, so the server never assigns an ID that could cause an AAD mismatch. The client-generated ID is baked into the cryptographic AAD at creation time and verified at every decryption.
Key Zeroization
All key material is zeroized (overwritten with zeros) when it goes out of scope. The Rust crate zeroize with ZeroizeOnDrop is used for the Account Key, ECDH private keys, and shared secrets. The Master Key and Encryption Key exist in memory only for the duration of the unlock operation and are immediately zeroized after the Account Key is unwrapped.