Per-Record Encryption
SyVault encrypts every record with its own unique Data Encryption Key (DEK). This is a deliberate architectural choice that provides stronger security guarantees than vault-level encryption, at the cost of slightly more key management complexity -- complexity that is handled transparently by the client.
Why Per-Record Matters
Most password managers use a single key to encrypt the entire vault (or a single key per vault). This means that if an attacker obtains that one key, every credential in the vault is exposed. SyVault takes a different approach.
| Attack Scenario | Vault-Level Encryption | Per-Record Encryption (SyVault) |
|---|---|---|
| Single key compromised | All records exposed | Only one record exposed |
| Memory dump during decryption | All DEKs in memory at once | Only active record's DEK in memory |
| Key rotation needed | Re-encrypt entire vault | Re-encrypt only the affected record |
| Sharing a record | Share vault key (exposes everything) or re-encrypt | Share only the record's DEK |
| Audit granularity | Key access = vault access | Key access = single record access |
How It Works
When you create a record:
- Generate a random DEK: a 256-bit key from a CSPRNG (cryptographically secure pseudorandom number generator).
- Encrypt the payload: the record's JSON payload is encrypted with the DEK using AES-256-GCM with AAD
syvault.record.{record_id}.payload.v1. - Wrap the DEK: the DEK is encrypted with the Vault Key using AES-256-GCM with AAD
syvault.record.{record_id}.dek.v1. - Store: both the wrapped DEK and the encrypted payload are sent to the server.
When you read a record:
- Unwrap the DEK: decrypt the wrapped DEK using the Vault Key and AAD
syvault.record.{record_id}.dek.v1. - Decrypt the payload: decrypt the encrypted payload using the DEK and AAD
syvault.record.{record_id}.payload.v1. - Zeroize: the DEK is zeroized from memory as soon as the payload is decrypted.
The record ID is embedded in the AAD at both the DEK-wrapping and payload-encryption layers. This creates a cryptographic binding between the record's identity and its encrypted data. An attacker cannot swap the wrapped DEK of record A into record B's slot -- the AAD mismatch causes authentication tag verification to fail.
AAD Strings from the Codebase
These are the actual AAD strings used in SyVault's Rust core (crates/core/src/vault/record.rs and crates/core/src/keys/hierarchy.rs):
// DEK wrapping (Vault Key encrypts DEK)
let aad = format!("syvault.record.{}.dek.v1", record_id);
// Payload encryption (DEK encrypts record JSON)
let payload_aad = format!("syvault.record.{}.payload.v1", record_id);
The record_id is a client-generated UUIDv4, ensuring the server cannot manipulate the ID to cause an AAD mismatch.
Comparison with Other Password Managers
Bitwarden
Bitwarden uses a single symmetric key (the "encryption key") derived from the master password to encrypt all vault items. While items are encrypted individually, they all share the same key. Compromising the encryption key exposes the entire vault.
1Password
1Password uses a two-secret model (master password + Secret Key) and derives a single vault key. Like Bitwarden, all items within a vault share the same encryption key.
Dashlane
Dashlane derives a single master key and encrypts vault items under it. Similar vault-level key model.
SyVault
SyVault adds an additional layer: each record gets its own random DEK, wrapped by the vault key. This means the vault key is a key-wrapping key, not a data-encryption key. Even if a vault key leaks, the attacker must still unwrap each record's DEK individually -- and because DEKs are 256-bit random values, there is no shortcut.
Per-record encryption also enables granular sharing. When you share a single record, SyVault shares only that record's DEK (re-wrapped for the recipient via ECDH). The recipient never receives your vault key and cannot decrypt any other records.
Memory Safety
During normal operation, only the DEK for the currently viewed record is held in memory. SyVault does not pre-decrypt all records at once. The DEK is zeroized (via the zeroize crate in Rust, and crypto.subtle in the browser which keeps key material in non-extractable CryptoKey objects where possible) immediately after use.