Complete the core zero-knowledge property: all user data (medications,
appointments, profile names) is now client-encrypted via AES-GCM; the server
stores and returns opaque ciphertext and can never decrypt it.
Frontend crypto module (Web Crypto API, no deps):
- crypto/keys.ts: double-PBKDF2 derivation from the password — an auth secret
(base64, sent to the server as the 'password') and an encryption key (AES-GCM
CryptoKey, kept in memory only, never transmitted). In-memory key store.
- crypto/cipher.ts: AES-GCM encrypt/decrypt + JSON convenience wrappers.
Auth split: login/register now derive the auth secret + enc key from the
password BEFORE the API call. Only the auth secret (not the raw password) is
sent to the server. The server's PBKDF2 stays as-is (it hashes whatever it
receives) but can never derive the enc key.
Backend — server treats all data blobs as opaque:
- Medication: removed MedicationData + flat MedicationResponse; new
MedicationResponse echoes metadata + encrypted_data blob. Create/update
accept opaque blobs (whole-blob replace). MedicationData struct deleted.
- Appointment: same opaque treatment; status moved to a top-level document field
so it remains filterable without decryption. AppointmentData struct deleted.
- Profile: name is now an opaque encrypted blob (name_data/name_iv). Auto-created
profile starts empty; client sets it.
- EncryptedFieldWire shared wire type across medication/appointment.
Frontend — decrypt-on-read, encrypt-on-write:
- Stores derive the enc key on login/register; decrypt wire responses into
domain objects on load; encrypt domain data into blobs on create/update.
- API client returns wire types (opaque blobs); components consume decrypted
domain data (mostly unchanged — the store does the crypto).
- Updated store tests for the ZK contract (derive a real key, mock wire responses).
- 20 frontend tests pass, build clean.
Backend: 21 tests pass (removed MedicationData/appointment-data deser tests;
opaque-blob echo tests added), clippy 0 warnings.
KNOWN LIMITATIONS (Phase 2): forgotten password = data loss (no recovery wrapping
yet). Page reload requires re-entering the password to re-derive the enc key
(in-memory only, by design). No data migration (no real data existed).
Backend: server can no longer read user data. All data blobs (medication,
appointment, profile name) are now opaque client-encrypted ciphertext — the
server stores and returns them verbatim, never deserializing the contents.
- Medication: removed MedicationData + flat MedicationResponse; new
MedicationResponse echoes metadata + encrypted_data blob. Create/update
accept opaque blobs (whole-blob replace). Update is no longer load-mutate-
reserialize (server can't read the data).
- Appointment: same opaque treatment; status moved to a top-level document
field so it remains filterable without decryption.
- Profile: name is now an opaque encrypted blob (name_data/name_iv). Auto-
created profile on register starts with an empty name; client sets it.
- EncryptedFieldWire type shared across medication/appointment.
Frontend (partial): crypto module using Web Crypto API —
- crypto/keys.ts: double-PBKDF2 derivation (auth secret sent to server +
encryption key kept in memory); in-memory key store (set/get/clear).
- crypto/cipher.ts: AES-GCM encrypt/decrypt + JSON convenience wrappers.
- crypto/index.ts: re-exports.
NOT YET DONE (frontend integration): auth store key derivation on login/register,
stores decrypt-on-load/encrypt-on-write, types update, UI components wired,
crypto round-trip tests, ADR. This commit is a verified checkpoint — backend
builds clean (21 tests, 0 warnings); frontend crypto module exists but is not
yet wired into the data flow.