Skip to content

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.

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.
  • encryptCredentialsForStorage also uses deriveUserKey (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, if cachedVersion !== payloadVersion, a SENSITIVE_DATA_KEY_VERSION_MISMATCH error 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 and users.cryptoKeyVersion are 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.

Distribuído sob licença MIT.