feat(THU-787): E2EE v2 AK/DEK key hierarchy + seamless v1→v2 migration - #1223
feat(THU-787): E2EE v2 AK/DEK key hierarchy + seamless v1→v2 migration#1223raivieiraadriano92 wants to merge 50 commits into
Conversation
Semgrep Security ScanNo security issues found. |
PR Metrics
Updated Thu, 27 Aug 2026 20:46:13 GMT · run #2810 |
49da24b to
820b6f3
Compare
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 2d8f7b34e811 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
Additional notes (couldn't anchor to a diff line)
backend/src/api/account.ts— 🚫 Blocking — Revoking a device on a not-yet-migrated v1 account fails with 403src/services/encryption.ts— 🔧 Nit — AK persisted before the DEK keyring in follow/approve paths — no encode self-heal if it crashes between
d14bd77 to
d392b76
Compare
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 8121499945c0 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
| switch (message.type) { | ||
| case 'invalidate': | ||
| dropKeyringCaches() | ||
| break |
There was a problem hiding this comment.
📐 Convention — DEK unwrap failure is swallowed with no logging
In resolveDEK the unwrap is wrapped in a bare catch {} that collapses every possible error into { failure: 'unwrap-failed' } without logging anything. A genuinely unexpected error (a corrupted wrapped blob, a WebCrypto/platform hiccup) then looks identical to the ordinary post-revocation wrong-AK case, and we lose the underlying cause entirely. Since the house style leans toward letting errors surface, could we at least console.warn(err) here (or bind the error) so a real fault isn't invisible during debugging? The expected wrong-key case can still map to the same failure without discarding the cause.
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 5dd8b77debe3 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
Additional notes (couldn't anchor to a diff line)
backend/src/api/account.ts:23— 📐 Convention — The ChallengeProof Elysia validator is copy-pasted into two route filesbackend/src/lib/canary.ts:78— 🔧 Nit — Docstring claims a failed proof "burns the nonce", but in-transaction callers roll it backsrc/services/encryption.ts— 📐 Convention — Raw local-DB scan lives in the service layer instead of the DAL
af56512 to
438b058
Compare
5dd8b77 to
0293182
Compare
|
Preview environment deployed 🚀
Stack: Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — |
THU-787 — E2EE v2 (AK/DEK key hierarchy) + seamless v1→v2 migration
Replaces the single-content-key v1 scheme with a versioned AK/DEK keyring and adds a seamless, no-data-loss migration from existing v1 accounts. New writes are v2; existing v1 data stays readable indefinitely through a cheap dual-read path.
Stacked on the min-version-gate branch (
thu-796) — the backend gate is the rollout control that makes the account flip safe at fleet scale (below-min clients can't sync a flipped account). SettingMIN_APP_VERSIONis a deploy-time config change, not part of this diff.What changed (v1 → v2)
CK(AES-256-GCM)AK(AES-KW) derived from a 24-word seed +kdf_salt, wrapping a versioned DEK keyringCKdirectlyDEKperkey_id; old DEKs retained for reads__enc:<iv>:<ct>(no key id, no AAD)__enc:v2:<key_id>:<iv>:<ct>, AAD-bound (table‖column‖rowId‖keyId)CKAKMigration model — absorb + permanent dual-read
"v1"DEK slot (wrapped under the new AK), mints the AK + a new recovery phrase, writes an AK envelope for every trusted device, and atomically flipsscheme_version 1→2."v1"slot — no bulk re-upload."v1"slot must decrypt-consistency-match the device's own v1 CK) before adopting — so a hostile flip from a stolen session is a recoverable DoS, never a plaintext leak or data loss.Key safety properties
scheme_version 1→2(single flip, wins the concurrent-migrator race); the recovery phrase is shown only on a200."v1"slot, so loweringMIN_APP_VERSIONpauses new flips with zero data loss.Scope
86 files, ~13k/2.6k. Backend:
scheme_versioncolumn +0028migration,POST /encryption/upgrade, keyring/challenge DAL. Frontend: AK/DEK crypto primitives + key storage, versioned AAD-bound codec + dual-read, v2 services (setup/rotation/recovery + migrator/follower), app-init wiring, sync-setup wizard, encryption settings UI. Plus the v1→v2 e2e suite and updated architecture docs.Testing
Unit (crypto primitives, dual-read codec, key storage, migration service), backend (upgrade endpoint atomic flip / CAS / coverage rejection, challenge-response), and e2e (web-first migrator, desktop-first follower, offline device joins late, below-min device does not sync).
Rollout
Ship the v2 build with
MIN_APP_VERSIONunset (no behavior change). Once the build is live on every channel, setMIN_APP_VERSION→ v2 threshold: below-min clients are rejected by the backend gate and shown UpgradeRequired; v2 clients flip on first open, followers self-serve.