Phase 2 cutover: dual-read/dual-write for envelope-encrypted secrets

✅ merged · #6 · easterbrooka/mike ← easterbrooka/mike · opened 16d ago by easterbrooka · merged 16d ago by easterbrooka · self · +264-26 across 7 files · ↗ on GitHub

From the PR description

Wires the Phase 2 crypto modules into the request path. After this lands and the backfill runs, every read prefers ciphertext (with plaintext fallback for un-backfilled rows) and every write seals the value under the user's DEK alongside the legacy plaintext column. The legacy columns stay populated during the cutover soak so a rollback to a v6/v7 image remains safe; migration 002 drops them once we're comfortable.

Call sites:

  • lib/userSettings.ts (getUserApiKeys, getUserModelSettings) - select both column pairs, decrypt the _ct if present, else fall back to the plaintext column.
  • routes/user.ts - PUT /user/api-keys/:provider seals the value and writes to BOTH dbField and dbField_ct. GET /user/api-keys/status returns Boolean(plaintext || ct).
  • routes/workflows.ts - four touch points:
    • resolveWorkflowAccess: lookup by shared_with_email_hmac.
    • GET /workflows shared lookup: same swap. - GET /workflows/:id/shares: select both columns and decrypt _ct when present; response shape unchanged. - POST /workflows/:id/share: write all three columns (shared_with_email, _ct, _hmac); upsert onConflict swapped to (workflow_id, shared_with_email_hmac).
  • lib/chatTools.ts (buildWorkflowStore): same HMAC-index swap.
  • index.ts: in production, assertKmsConfigured() + EMAIL_HMAC_PEPPER presence check at startup so misconfigured deploys fail fast rather than 500-ing at the first read or write.

Supporting changes in lib/crypto/migrate.ts:

  • Export byteaToBuffer for call-site decoding of Supabase bytea reads.
  • Add getTenantCrypto() - a process-wide TenantCrypto singleton so the in-process DEK cache amortises KMS Decrypts across requests. Tests keep using tenantCrypto(fakeDb) directly.

Plus scripts/backfill_envelope.ts: the one-shot backfill that walks user_profiles and workflow_shares, mints DEKs as needed, and writes the _ct (and _hmac for shares) columns for any rows that still lack them. Idempotent. Runs on mike-builder with KMS_KEY_ID + EMAIL_HMAC_PEPPER set, against the same Supabase project as prod.

86/86 backend tests still passing; no test changes needed since the existing crypto-module tests cover the round-trip and the call-site changes are dual-path with the plaintext fallback exercised by the existing read paths.

Our analysis

Cut over the request path to envelope-encrypted columns — 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-6.md from inside the repo you want the changes in.

⬇ Download capture-pull-6.md