Records the decision: client-side AES-256-GCM, double-PBKDF2 auth/enc-key split, server-as-blind-store, in-memory key lifecycle, and Phase 1 limitations (forgotten password = data loss; Phase 2 recovery wrapping deferred).
2.7 KiB
ADR: Zero-Knowledge Encryption (Phase 1)
Status: Implemented (Phase 1) Date: 2026-06-28
Context
The original design (docs/product/encryption.md, docs/adr/mongodb-schema-decision.md)
specified client-side zero-knowledge encryption: the server should never hold
plaintext user data or encryption keys. Until this change, the EncryptedField
type and recovery-phrase fields were aspirational scaffolding — data was always
stored and processed as plaintext JSON.
Decision
Implement client-side zero-knowledge encryption using AES-256-GCM via the browser's Web Crypto API. The server becomes a blind store: it holds opaque ciphertext and can never decrypt user data.
Key derivation: double PBKDF2
From the user's password, the client derives two independent values:
- Auth secret:
PBKDF2(password, "normogen-auth-v1", 150k iters, SHA-256)→ base64, sent to the server as the "password". The server PBKDF2-hashes it (as before). - Encryption key:
PBKDF2(password, "normogen-enc-v1", 150k iters, SHA-256)→ AES-GCMCryptoKey, kept in memory only, never transmitted.
The server cannot derive the encryption key because it never sees the raw password or the enc-domain PBKDF2 output.
What's encrypted
- Medication data blobs (name, dosage, frequency, route, etc.)
- Appointment data blobs (title, provider, date/time, etc.)
- Profile display name
What stays plaintext (queryable)
- IDs (
medicationId,appointmentId,userId,profileId) - Medication
activeflag (top-level, filterable) - Appointment
status(top-level, filterable) - Timestamps
In-memory key lifecycle
The encryption key lives only in browser memory for the authenticated session. It is NOT persisted. A page reload requires re-entering the password to re-derive it. This is the core ZK trade-off.
Consequences
Phase 1 limitations (documented)
- Forgotten password = data loss. No recovery key-wrapping yet (Phase 2).
- Page reload requires re-authentication to re-derive the enc key.
- No data migration was needed (no real user data existed).
Phase 2 (future)
- Recovery key-wrapping (wrap the enc key under a recovery-derived key so a forgotten password doesn't lose data).
- Password-change re-wrapping (re-wrap the enc key under the new password).
- Wrapped-DEK model (separate data-encryption key + key-encryption key).
Relationship to the original design doc
docs/product/encryption.md describes a fuller zero-knowledge architecture
(per-user keys, deterministic encryption for search, key rotation). This Phase 1
implements the core property (server can't read data) with the simplest viable
design. The fuller features remain future work.