ADR-0004: Client-side encryption approach for sensitive local data
- Status: Accepted
- Date: 2026-04-11
Context
The app is backend-primary and stores sensitive auth/session/user data in IndexedDB as a fallback while offline.
Decision
Keep browser-native window.crypto.subtle primitives as the only client-side crypto baseline in app modules. Avoid provider-specific client SDK crypto wrappers.
Consequences
- Retains deterministic browser support and contributor-friendly review surface.
- Requires explicit key-lifecycle design in future hardening phases.
- Implementation now enforces secure defaults in the app layer:
- Sensitive IndexedDB fields persist as AES-GCM envelopes (
ciphertext,iv,keyVersion) instead of plaintext. - In-memory keys are cleared on logout and session expiry; protected actions require re-authentication to unlock sensitive local data again.
- Re-authentication rotates per-user encryption key version and re-encrypts session/event encrypted payloads.
- Decrypted values are not logged; audit-safe lifecycle events remain unchanged.
- Sensitive IndexedDB fields persist as AES-GCM envelopes (
References
- ../srs.md
- ../sdd.md
- ../../reference/data-dictionary.md
- Requirement IDs: NFR-007, SCR-004
Addendum — 2026-06-10
Key derivation and versioning — implementation details from encryptionService.ts:
Key derivation (deriveUserKey, lines 47–61):
encryptionService.deriveUserKey(userId, passwordHash, keyVersion)derives a 256-bit AES-GCM key from the password hash using PBKDF2 (600k iterations, SHA-256).- The salt is
neemias:${userId}:v${keyVersion}— user-scoped and version-scoped. encryptCredentialsForStoragealso usesderiveUserKey(not the cached key), so credentials can be decrypted even if the in-memory key context was cleared.
Key versioning (cryptoKeyVersion):
- Monotonic integer starting at 1. Increments on every key rotation.
- Each encrypted payload carries its
keyVersion— on decryption, ifcachedVersion !== payloadVersion, aSENSITIVE_DATA_KEY_VERSION_MISMATCHerror is thrown. The caller must re-derive with the correct version. - During rotation (
rotateEncryptedDataForUser): old-version sessions, studentEvents, and userEvents are decrypted and re-encrypted with the new key version. The cached key context andusers.cryptoKeyVersionare then updated.
Rotation interval (shouldRotateKey):
- 30 days since
keyRotatedAt, checked on every login and re-authentication. - Rotation is best-effort — if it fails, the session remains active rather than blocking login.