Add Phase 2 envelope-encryption foundations (migration + crypto modules)

✅ merged · #5 · easterbrooka/mike ← easterbrooka/mike · opened 16d ago by easterbrooka · merged 16d ago by easterbrooka · self · +1,710-414 across 11 files · ↗ on GitHub

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.

⬇ Download capture-pull-5.md