Skip to main content
Version: Next

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 ScenarioVault-Level EncryptionPer-Record Encryption (SyVault)
Single key compromisedAll records exposedOnly one record exposed
Memory dump during decryptionAll DEKs in memory at onceOnly active record's DEK in memory
Key rotation neededRe-encrypt entire vaultRe-encrypt only the affected record
Sharing a recordShare vault key (exposes everything) or re-encryptShare only the record's DEK
Audit granularityKey access = vault accessKey access = single record access

How It Works

When you create a record:

  1. Generate a random DEK: a 256-bit key from a CSPRNG (cryptographically secure pseudorandom number generator).
  2. 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.
  3. Wrap the DEK: the DEK is encrypted with the Vault Key using AES-256-GCM with AAD syvault.record.{record_id}.dek.v1.
  4. Store: both the wrapped DEK and the encrypted payload are sent to the server.

When you read a record:

  1. Unwrap the DEK: decrypt the wrapped DEK using the Vault Key and AAD syvault.record.{record_id}.dek.v1.
  2. Decrypt the payload: decrypt the encrypted payload using the DEK and AAD syvault.record.{record_id}.payload.v1.
  3. Zeroize: the DEK is zeroized from memory as soon as the payload is decrypted.
info

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.

tip

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.