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
↓ peach-browser-44585ddf.tar.gzatlas at this save
files changed6 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 −1manifest.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": {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",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",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 });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 }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;