44585ddffc10

fix: deadline and clear recovery confirmation exemption (PB-26-102, v5.0.33)

time
2026-09-01 20:40 UTC · 1 month ago
machine
koh2
agent
gpt-5
session
2026-09-01 security assessment remediation
parent
6fbc284fc874
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/recovery-pending.test.ts+27 −1src/background/recovery-pending.ts+12 −7src/background/vault.ts+5 −1
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.32",4+  "version": "5.0.33",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "5.0.32",5+  "version": "5.0.33",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.0.32",3+  "version": "5.0.33",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/recovery-pending.test.ts+27 −1
@@ -26,7 +26,9 @@2626 2727     await pending.setRecoveryConfirmPending();2828 29-    expect(session.set).toHaveBeenCalledWith({ peach_recovery_pending: "1" });29+    expect(session.set).toHaveBeenCalledWith({30+      peach_recovery_pending: expect.any(Number),31+    });3032     expect(local.set).not.toHaveBeenCalled();3133     await expect(pending.isRecoveryConfirmPending()).resolves.toBe(true);3234   });@@ -52,5 +54,29 @@5254     await pending.clearRecoveryConfirmPending();5355     await expect(pending.isRecoveryConfirmPending()).resolves.toBe(false);5456     expect(local.set).not.toHaveBeenCalled();57+  });58+59+  it("expires the recovery exemption after its hard deadline", async () => {60+    vi.useFakeTimers();61+    try {62+      const pending = await import("./recovery-pending");63+      await pending.setRecoveryConfirmPending();64+65+      await vi.advanceTimersByTimeAsync(10 * 60 * 1000 + 1);66+67+      await expect(pending.isRecoveryConfirmPending()).resolves.toBe(false);68+      expect(session.remove).toHaveBeenCalledWith("peach_recovery_pending");69+    } finally {70+      vi.useRealTimers();71+    }72+  });73+74+  it("clears the recovery exemption when the vault locks", async () => {75+    const pending = await import("./recovery-pending");76+    const vault = await import("./vault");77+    await pending.setRecoveryConfirmPending();78+79+    vault.lock();80+    await vi.waitFor(() => expect(sessionValues.peach_recovery_pending).toBeUndefined());5581   });5682 });

view file

src/background/recovery-pending.ts+12 −7
@@ -6,7 +6,8 @@66 // Firefox falls back to this background module's memory for the same process.77 88 const KEY = "peach_recovery_pending";9-let inMemoryPending = false;9+const RECOVERY_CONFIRM_WINDOW_MS = 10 * 60 * 1000;10+let inMemoryDeadline = 0;1011 1112 function sessionStorage(): chrome.storage.StorageArea | undefined {1213   return chrome.storage?.session;@@ -15,23 +16,27 @@1516 export async function isRecoveryConfirmPending(): Promise<boolean> {1617   try {1718     const session = sessionStorage();18-    if (!session) return inMemoryPending;19+    if (!session) return inMemoryDeadline > Date.now();1920     const out = await session.get(KEY);20-    return (out as Record<string, unknown>)[KEY] === "1";21+    const deadline = (out as Record<string, unknown>)[KEY];22+    if (typeof deadline === "number" && deadline > Date.now()) return true;23+    await clearRecoveryConfirmPending();24+    return false;2125   } catch {22-    return inMemoryPending;26+    return inMemoryDeadline > Date.now();2327   }2428 }2529 2630 export async function setRecoveryConfirmPending(): Promise<void> {31+  const deadline = Date.now() + RECOVERY_CONFIRM_WINDOW_MS;2732   try {2833     const session = sessionStorage();2934     if (session) {30-      await session.set({ [KEY]: "1" });35+      await session.set({ [KEY]: deadline });3136       return;3237     }3338   } catch { /* use the in-memory fallback */ }34-  inMemoryPending = true;39+  inMemoryDeadline = deadline;3540 }3641 3742 export async function clearRecoveryConfirmPending(): Promise<void> {@@ -41,5 +46,5 @@4146       await session.remove(KEY);4247     }4348   } catch { /* best-effort */ }44-  inMemoryPending = false;49+  inMemoryDeadline = 0;4550 }

view file

src/background/vault.ts+5 −1
@@ -35,7 +35,10 @@3535 } from "../shared/types";3636 import { decrypt, encrypt, zeroKey } from "./aes";3737 import { clearAutoLock, touchAutoLock } from "./alarms";38-import { setRecoveryConfirmPending } from "./recovery-pending";38+import {39+  clearRecoveryConfirmPending,40+  setRecoveryConfirmPending,41+} from "./recovery-pending";3942 4043 type EntryDraft = Omit<4144   VaultEntry,@@ -1512,6 +1515,7 @@15121515   // not worth an unlocked vault the user never asked to unlock; when locked,15131516   // sync-runner correctly reports VAULT_LOCKED instead.15141517   void clearSessionKey();1518+  void clearRecoveryConfirmPending();15151519   if (derivedKey) {15161520     zeroKey(derivedKey);15171521     derivedKey = null;

view file