Add Phase 2 envelope-encryption foundations (migration + crypto modules)
From the PR description
Pure additions; no app behaviour change. The new code isn't imported by the running backend yet - the call-site dual-read/dual-write changes ship in a follow-up so they can be reviewed in isolation.
What lands:
- Migration 001_envelope_encryption.sql (purely additive): - tenant_deks table with one active wrapped DEK per user - user_profiles.{claude,gemini}_api_key_ct bytea - workflow_shares.shared_with_email_ct bytea - workflow_shares.shared_with_email_hmac bytea + unique index on (workflow_id, hmac) to preserve the existing share-uniqueness invariant during the dual-write window. Old plaintext columns and old unique constraint are kept, dropped in 002 once the ciphertext path is verified in production.
- backend/src/lib/crypto/aead.ts - AES-256-GCM with a self-describing envelope: version (1B) | dek_id (4B BE) | iv (12B) | ct | tag (16B). Version byte mandatory and checked on every open(); v2 reserved for AAD binding (table/column/row) once we need it.
- backend/src/lib/crypto/searchable.ts - HMAC-SHA256(pepper, normalised email) for the workflow_shares deterministic email index. Pepper held in Secrets Manager (mike/email-hmac-pepper, set up in a follow-up); never written to the DB. Pepper rotation is treated as a stop-the-world maintenance op in exchange for bare 32-byte HMACs.
- backend/src/lib/crypto/kms.ts - KMS GenerateDataKey/Decrypt wrapper. KEK is alias/mike-app-data (created in follow-up). assertKmsConfigured() for fail-fast at startup.
- backend/src/lib/crypto/migrate.ts - TenantCrypto, the wrapper call sites import. Lazy DEK creation, in-process LRU cache of plaintext DEKs by dek_id (cap 1024) so KMS Decrypt is at most one call per dek_id per process lifetime.
- @aws-sdk/client-kms added to backend dependencies.
- 41 new unit tests; full suite 86/86 passing.
Threat model deliberately scoped: encryption defends against DB-only breach (Supabase compromise, leaked dump, rogue read replica). Service role bypasses RLS so a leaked SUPABASE_SECRET_KEY remains a full compromise - not changed by this work.
Deferred to follow-ups (with handoff doc):
- AWS infra: create alias/mike-app-data + mike/email-hmac-pepper
- Apply this migration to prod Supabase (additive, zero-downtime)
- Backfill script + task def :8 with KMS_KEY_ID, EMAIL_HMAC_PEPPER
- Call-site dual-read/dual-write changes (userSettings.ts, routes/user.ts, routes/workflows.ts, chatTools.ts) + redeploy
- Migration 002 dropping the plaintext columns once verified
Our analysis
Land envelope encryption primitives without wiring them up — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-5.md from
inside the repo you want the changes in.