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

<!-- SECTION:DESCRIPTION:BEGIN -->
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.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [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
<!-- AC:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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.
<!-- SECTION:FINAL_SUMMARY:END -->
