--- id: TASK-124 title: Encrypt all PII at rest in EncryptID database status: Done assignee: [] created_date: '2026-03-24 00:29' updated_date: '2026-03-24 00:29' labels: - security - encryptid - database dependencies: [] references: - src/encryptid/server-crypto.ts - src/encryptid/migrations/encrypt-pii.ts priority: high --- ## Description Server-side AES-256-GCM encryption for all PII fields stored in PostgreSQL. Keys derived from JWT_SECRET via HKDF with dedicated salts (`pii-v1` for encryption, `pii-hash-v1` for HMAC). HMAC-SHA256 hash indexes for equality lookups on email and UP address fields. **Scope:** 18 fields across 6 tables (users, guardians, identity_invites, space_invites, notifications, fund_claims). Username and display_name excluded (public identifiers, needed for ILIKE search). **Files:** - `src/encryptid/server-crypto.ts` — NEW: encryptField(), decryptField(), hashForLookup() - `src/encryptid/schema.sql` — 18 _enc/_hash columns + 4 indexes - `src/encryptid/db.ts` — async row mappers with decrypt fallback, dual-write on inserts/updates, hash-based lookups - `src/encryptid/server.ts` — replaced unkeyed hashEmail() with HMAC hashForLookup() - `src/encryptid/migrations/encrypt-pii.ts` — NEW: idempotent backfill script **Remaining:** Drop plaintext columns after extended verification period. ## Acceptance Criteria - [x] #1 All PII fields have corresponding _enc columns with AES-256-GCM ciphertext - [x] #2 HMAC-SHA256 hash indexes enable email and UP address lookups without plaintext - [x] #3 Row mappers decrypt transparently — callers receive plaintext - [x] #4 Wrong encryption key cannot decrypt (verified with test) - [x] #5 Same plaintext produces different ciphertext each time (random IV) - [x] #6 Backfill migration encrypts all existing rows (0 remaining unencrypted) - [x] #7 Legacy plaintext fallback works for pre-migration rows during transition ## Final Summary Deployed 2026-03-23. Commit `9695e95`. Backfill completed: 1 user, 2 guardians, 8 identity invites, 2 fund claims encrypted. 19/19 verification tests passed (ciphertext format, decryption, HMAC determinism, wrong-key rejection, random IV uniqueness). Plaintext columns retained for rollback safety — drop in follow-up task after extended verification.