Skip to main content
Version: Next

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:

ParameterValue
Memory64 MiB (65,536 KiB)
Iterations3
Parallelism4 lanes
Output length32 bytes (256 bits)
Salt16 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.
danger

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.

info

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.