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). Conceptually, the AAD for a note content cell binds the table, column, and row ID together. Roughly:

notes + content + <row-uuid>

(The on-the-wire form is a null-separated binary blob, <table>\0<column>\0<16-byte-uuid>, not the readable string above; the readable version is shown for illustration.)

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.