Skip to content

Encryption Model

Engram encrypts all sensitive data at rest. The design is operator-friendly without being naive about how encryption fails in practice.

Engram uses a two-key hierarchy:

master key (env) → wraps → per-user DEK (db) → encrypts → user data
  • Master key lives in ENCRYPTION_MASTER_KEY (env var, never on disk in plaintext). Used only to wrap/unwrap DEKs in memory.
  • Per-user DEK is a random AES-256 key generated at user creation. It encrypts that user’s notes, attachments, search payloads, and metadata. Stored in the database wrapped by the master key.

The split lets master-key rotation re-wrap every DEK without re-encrypting user data, and lets a single user’s DEK rotate without touching anyone else’s.

The same model gives multi-tenancy for free: a bug that leaked one user’s bytes still wouldn’t decrypt without that user’s DEK.

DataEncryptedNotes
Note contentAES-256-GCM with per-row AAD
Note titleSame
Note pathsHMACStored as HMAC for indexing; plaintext not in DB
Folders/tagsHMACSame
Attachments (S3 blobs)Encrypted client-side before S3 PUT
Qdrant payloadsSearch-index metadata encrypted per-chunk
User emailPlaintext (needed for login lookup; redacted in logs but stored unencrypted in the users table)
User passwordsbcrypt’d (separate from DEK chain)
Wrapped DEKsAES-256-GCM wrap with master key

The wire format is versioned: <version_byte, nonce, ciphertext, auth_tag>. The version byte lets the read path support old and new formats simultaneously during a migration.

AAD binding (the “no-rowswap” guarantee)

Section titled “AAD binding (the “no-rowswap” guarantee)”

Every encrypted column also commits to its row identity via AAD (Additional Authenticated Data). Specifically, the AAD for a note content cell is:

notes:content:<row_id>

Decrypt fails if the AAD doesn’t match — so if an attacker tries to copy ciphertext from one row to another, the decrypt fails closed. Encryption isn’t just confidentiality; it’s also a row-binding integrity check.

On every app start, Engram tries to decrypt a known sentinel value stored in the system_canaries table. If decrypt fails, the master key is wrong — and the app refuses to boot. This means you can’t accidentally run with a half-rotated key and silently corrupt new writes.

  • The master key is the keystone. Lose it → lose all user data. Back it up offline.
  • Database leaks are bounded. A stolen DB dump without the master key is useless ciphertext.
  • Tampering is detected. AAD binding + auth tags mean a partial DB write or a malicious modification fails closed at read time.

Operational runbooks for rotation, half-state recovery, and key backup live in Self-Host → Encryption Setup.