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).