test: E2E ZK integration tests + frontend lifecycle test
Backend ZK integration tests (tests/zk_integration_tests.rs): - medication_stored_as_ciphertext: register → create med with opaque blob → response echoes blob, no plaintext fields leaked → GET echoes blob. - appointment_stored_as_ciphertext_with_top_level_status: opaque blob + status filter works server-side without decryption. - health_stat_stored_as_ciphertext: opaque blob, no value/stat_type leaked. - dose_schedule_adherence_reflects_missed_doses: 1×/day schedule → 30 scheduled, 1 taken, 29 missed, ~3.3% rate. Updated medication_tests.rs for the opaque-blob contract (was sending old plaintext name/dosage fields). Frontend lifecycle test (useStore.test.ts): - Full ZK round-trip with real WebCrypto: setup → encrypt → verify ciphertext → unlock with password → decrypt → recover via phrase → rewrap under new password → decrypt with new key. Data survives the full lifecycle. Verified: backend 24 tests 0 warnings; frontend 25 tests, build clean.
This commit is contained in:
parent
288e776a8c
commit
34e3b0b0e0
3 changed files with 351 additions and 12 deletions
|
|
@ -1,6 +1,14 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { deriveAuthAndEncKeys, setEncKey, clearEncKey } from '../crypto';
|
||||
import { encryptJson } from '../crypto';
|
||||
import {
|
||||
deriveAuthAndEncKeys,
|
||||
setEncKey,
|
||||
clearEncKey,
|
||||
encryptJson,
|
||||
setupEncryption,
|
||||
unlockWithPassword,
|
||||
unlockWithRecovery,
|
||||
rewrapDek,
|
||||
} from '../crypto';
|
||||
|
||||
// Mock the api client so the store's real reducer logic is exercised without HTTP.
|
||||
const apiMock = {
|
||||
|
|
@ -117,3 +125,57 @@ describe('useMedicationStore', () => {
|
|||
expect(useMedicationStore.getState().adherence['m1'].adherence_rate).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zero-knowledge medication lifecycle (register → create → recover)', () => {
|
||||
// This test exercises the full ZK flow using real WebCrypto + the real store,
|
||||
// with mocked HTTP. It proves: encrypt-on-write, decrypt-on-read, and that
|
||||
// data encrypted under one key can be recovered via the recovery phrase and
|
||||
// decrypted under a re-wrapped key.
|
||||
it('full lifecycle: data survives password recovery', async () => {
|
||||
const password = 'lifecycle-password';
|
||||
const recoveryPhrase = 'lifecycle-recovery';
|
||||
const medData = { name: 'Lisinopril', dosage: '10mg', frequency: 'daily' };
|
||||
|
||||
// 1. Setup encryption (register): generate DEK, wrap under password + recovery.
|
||||
const setup = await setupEncryption(password, recoveryPhrase);
|
||||
setEncKey(setup.dek);
|
||||
|
||||
// 2. Encrypt the medication data (what the store does on create).
|
||||
const encrypted = await encryptJson(medData, setup.dek);
|
||||
expect(encrypted.data).not.toContain('Lisinopril'); // ciphertext, not plaintext
|
||||
|
||||
// 3. Simulate page reload: DEK is lost.
|
||||
clearEncKey();
|
||||
|
||||
// 4. Unlock with password (the unlock screen).
|
||||
const unlockResult = await unlockWithPassword(password, setup.passwordWrappedDek);
|
||||
setEncKey(unlockResult.dek);
|
||||
|
||||
// 5. Decrypt the data with the unlocked DEK.
|
||||
const decrypted1 = await encryptJson(medData, unlockResult.dek).then((enc) =>
|
||||
// We can't decrypt what we just encrypted in the same call; use the
|
||||
// original encrypted blob instead.
|
||||
Promise.resolve(enc),
|
||||
);
|
||||
// Use the ORIGINAL encrypted blob (from step 2) to verify round-trip.
|
||||
const { decryptJson } = await import('../crypto');
|
||||
const roundTripped = await decryptJson<typeof medData>(encrypted, unlockResult.dek);
|
||||
expect(roundTripped.name).toBe('Lisinopril');
|
||||
|
||||
// 6. Simulate forgot-password: recover via recovery phrase.
|
||||
clearEncKey();
|
||||
const recoveredDek = await unlockWithRecovery(recoveryPhrase, setup.recoveryWrappedDek!);
|
||||
const recoveredData = await decryptJson<typeof medData>(encrypted, recoveredDek);
|
||||
expect(recoveredData.name).toBe('Lisinopril');
|
||||
expect(recoveredData.dosage).toBe('10mg');
|
||||
|
||||
// 7. Re-wrap under a new password (post-recovery).
|
||||
const newPassword = 'new-lifecycle-password';
|
||||
const rewrapped = await rewrapDek(recoveredDek, newPassword);
|
||||
const newUnlock = await unlockWithPassword(newPassword, rewrapped);
|
||||
const finalData = await decryptJson<typeof medData>(encrypted, newUnlock.dek);
|
||||
expect(finalData.name).toBe('Lisinopril');
|
||||
|
||||
clearEncKey();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue