cdac7526ea77
fix: PCH-01-009 rehydrate rootKey in unlockWithKey biometric path via deriveRootKeyFromDerivedKey — sync + recovery-word access work after biometric unlock (v3.8.1)
- time
- 2026-06-10 19:49 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- pch-01 fix integration
- parent
- 14cea33605e5
↓ peach-browser-cdac7526.tar.gzatlas at this save
files changed2 files
src/background/vault.test.ts+31 −0src/background/vault.ts+11 −0src/background/vault.test.ts+31 −0
@@ -1035,6 +1035,37 @@10351035 expect(vault.getRootKeyHex()).toBeNull();10361036 });10371037 1038+ it("unlock_with_key_rehydrates_root_key (PCH-01-009)", async () => {1039+ const vault = await import("./vault");1040+ await vault.createVault("biometric-pass-12345");1041+ const rootBefore = vault.getRootKeyHex();1042+ const keyHex = vault.getDerivedKeyHex();1043+ expect(rootBefore).not.toBeNull();1044+ vault.lock();1045+ // The biometric path passes the pre-derived AES key; rootKey must rehydrate.1046+ const r = await vault.unlockWithKey(keyHex);1047+ expect(r.ok).toBe(true);1048+ expect(vault.getRootKeyHex()).toBe(rootBefore);1049+ });1050+1051+ it("unlock_with_key_succeeds_without_escrow (PCH-01-009)", async () => {1052+ const vault = await import("./vault");1053+ await vault.createVault("legacy-pass-12345");1054+ const keyHex = vault.getDerivedKeyHex();1055+ vault.lock();1056+ // Strip the recovery escrow to mimic a pre-v2.12 slot — unlock must still1057+ // succeed (non-fatal), leaving rootKey null.1058+ const slot = storage.peach_vault_a as Record<string, unknown>;1059+ delete slot.recovery_salt;1060+ delete slot.recovery_wrapped_root_key;1061+ delete slot.pw_root_ciphertext;1062+ delete slot.pw_root_nonce;1063+ delete slot.pw_root_tag;1064+ const r = await vault.unlockWithKey(keyHex);1065+ expect(r.ok).toBe(true);1066+ expect(vault.getRootKeyHex()).toBeNull();1067+ });1068+10381069 it("duress keeps both slots equal length at rest, even as the real vault grows (audit MEDIUM)", async () => {10391070 const vault = await import("./vault");10401071 await vault.createVault("duress-real-pass-aaaa");src/background/vault.ts+11 −0
@@ -1312,6 +1312,17 @@13121312 buildDomainIndexSync(entries);13131313 await loadSettings();13141314 touchAutoLock();1315+1316+ // PCH-01-009: the biometric path skips Argon2id but must still rehydrate the1317+ // root key, exactly like attemptUnlockWithDerive and restoreFromSession.1318+ // Without it, rootKey stays null after a biometric unlock and chirp sync +1319+ // GET_RECOVERY_WORDS report the open vault as locked. Guarded and non-fatal:1320+ // a missing/legacy escrow must not fail the unlock itself.1321+ if (stored.recovery_salt && stored.recovery_wrapped_root_key) {1322+ try {1323+ rootKey = await deriveRootKeyFromDerivedKey(masterKeyBytes, stored);1324+ } catch { /* non-fatal — unlock still succeeds without rootKey */ }1325+ }13151326 return { ok: true, entries };13161327 } catch {13171328 return { ok: false, error: "invalid_key" };