fix: replace SHA-256 with HKDF + per-row salt for user API key encryption

🟢 open · #76 · willchen96/mike ← bmersereau/mike · opened 14d ago by bmersereau · +1,371-23 across 7 files · ↗ on GitHub

From the PR description

Summary

  • Replaces the single-SHA-256 encryptionKey() with HKDF (RFC 5869) and a per-row random 16-byte salt, eliminating the one-hash-breaks-all vulnerability
  • Backward-compatible: rows with salt IS NULL still decrypt via the legacy SHA-256 path; new and updated rows automatically use HKDF
  • Adds a salt text column to the user_api_keys table (nullable for compatibility with existing rows)

Closes #67

Changes

  • backend/src/lib/userApiKeys.ts - encryptKey/decryptKey exported helpers; deriveKey uses hkdfSync with per-row salt; legacyKey covers null-salt rows
  • backend/schema.sql - salt text column added to user_api_keys; run ALTER TABLE public.user_api_keys ADD COLUMN IF NOT EXISTS salt text; on existing databases
  • backend/vitest.config.ts + backend/package.json - vitest test runner added
  • backend/src/lib/__tests__/userApiKeys.test.ts - 5 unit tests covering HKDF round-trip, unique salts/IVs, tamper detection, and legacy SHA-256 compat

Test plan

  • Unit tests: encrypt→decrypt round-trip, unique salt per call, tamper detection, legacy null-salt backward compat
  • Build and typecheck pass

Our analysis

Replace SHA-256 key derivation with HKDF and per-row salt — 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-76.md from inside the repo you want the changes in.

⬇ Download capture-pull-76.md