e1bf91ef908d
fix: LV-4 GET_RECOVERY_WORDS requires fresh password re-auth (verifyMainPassword), exempt during recovery-pending confirmation window; change-password flow sets pending
- time
- 2026-06-10 02:43 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- audit fix integration
- parent
- 0c436fd73d57
↓ peach-browser-e1bf91ef.tar.gzatlas at this save
files changed3 files
src/background/messages.test.ts+34 −0src/background/messages.ts+18 −0src/popup/views/Settings.tsx+5 −0src/background/messages.test.ts+34 −0
@@ -1112,4 +1112,38 @@11121112 expect(await findPairedDeviceById("android-new")).toBeNull();11131113 expect(await listPendingVouchedDevices()).toHaveLength(0);11141114 });1115+1116+ // --- LV-4: GET_RECOVERY_WORDS requires a fresh password proof ---1117+1118+ it("LV-4: GET_RECOVERY_WORDS without a password returns password_required", async () => {1119+ const vault = await import("./vault");1120+ await vault.createVault("correct-pass-12345");1121+1122+ const res = await dispatch({ type: "GET_RECOVERY_WORDS", payload: {} });1123+ expect(res).toMatchObject({ ok: false, error: "password_required" });1124+ });1125+1126+ it("LV-4: GET_RECOVERY_WORDS with a wrong password returns wrong_password and no words", async () => {1127+ const vault = await import("./vault");1128+ await vault.createVault("correct-pass-12345");1129+1130+ const res = await dispatch({1131+ type: "GET_RECOVERY_WORDS",1132+ payload: { password: "not-the-password" },1133+ });1134+ expect(res).toMatchObject({ ok: false, error: "wrong_password" });1135+ expect(res.data).toBeUndefined();1136+ });1137+1138+ it("LV-4: GET_RECOVERY_WORDS with the correct password returns 12 words", async () => {1139+ const vault = await import("./vault");1140+ await vault.createVault("correct-pass-12345");1141+1142+ const res = await dispatch({1143+ type: "GET_RECOVERY_WORDS",1144+ payload: { password: "correct-pass-12345" },1145+ });1146+ expect(res.ok).toBe(true);1147+ expect((res.data as string[]).length).toBe(12);1148+ });11151149 });src/background/messages.ts+18 −0
@@ -17,6 +17,7 @@1717 listPendingVouchedDevices,1818 removePendingVouchedDevice,1919 } from "./pending-vouched-devices";20+import { isRecoveryConfirmPending } from "./recovery-pending";2021 import { setupAutoLock, touchAutoLock } from "./alarms";2122 import { runSelfTest } from "./self-test";2223 import * as vault from "./vault";@@ -311,6 +312,23 @@311312 case "GET_RECOVERY_WORDS": {312313 // Must be unlocked and have recovery set up (vault must be v2.12+).313314 if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };315+ // LV-4: the recovery words re-derive the ROOT KEY — the highest-316+ // value secret in the vault. A session unlock (which may be hours317+ // old on a shared machine) is not enough; require a fresh password318+ // proof, matching the 1Password/Bitwarden pattern for revealing319+ // escrow material. EXEMPTION: the create-vault / rotation confirmation320+ // window (peach_recovery_pending) shows the words immediately after321+ // the user authenticated, and that confirmation UI has no password to322+ // resend — skip the re-auth there only.323+ if (!(await isRecoveryConfirmPending())) {324+ const { password } = (msg.payload ?? {}) as { password?: string };325+ if (typeof password !== "string" || !password) {326+ return { ok: false, error: "password_required" };327+ }328+ if (!(await vault.verifyMainPassword(password))) {329+ return { ok: false, error: "wrong_password" };330+ }331+ }314332 try {315333 const words = await vault.getRecoveryWords();316334 return { ok: true, data: words };src/popup/views/Settings.tsx+5 −0
@@ -481,6 +481,11 @@481481 if (res.ok) {482482 setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm("");483483 setShowChangePw(false);484+ // LV-4: the user just authenticated (current password), so the recovery485+ // words reveal that follows is exempt from the re-auth gate. Set the486+ // pending flag the same way the create/recovery flow does; the display487+ // clears it on confirm.488+ await chrome.storage?.local?.set?.({ peach_recovery_pending: "1" });484489 setShowNewRecoveryWords(true);485490 } else {486491 setChangePwError(res.error ?? "failed to change password");