371dbc476bcb

close PB-26-925 and PB-26-927 through PB-26-929 at Browser 5.2.51; record PB-26-926 Android gate blocker

time
2026-09-12 01:39 UTC · 1 month ago
machine
koh2
agent
camel-stream/auto
session
close audit findings
parent
bea130facd46
13 files Dockerfile.firefox-gate+2 −6README.md+2 −3manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/message-policy.test.ts+1 −1src/background/message-policy.ts+1 −1src/background/messages.ts+12 −8src/background/sync-alerts.test.ts+11 −0src/background/sync-alerts.ts+15 −4src/popup/App.tsx+8 −8src/shared/release-notes.ts+9 −1src/shared/types.ts+2 −1
Dockerfile.firefox-gate+2 −6
@@ -1,10 +1,6 @@1-FROM oven/bun:1.3.14-debian1+FROM mcr.microsoft.com/playwright:v1.59.1-noble@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b5322 3-RUN apt-get update \4-  && apt-get install -y --no-install-recommends firefox-esr ca-certificates nodejs npm5-63 WORKDIR /app7-ENV FIREFOX_BIN=/usr/bin/firefox-esr4+ENV FIREFOX_BIN=/usr/bin/firefox85 ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright96 10-RUN bunx playwright install --with-deps firefox

view file

README.md+2 −3
@@ -48,9 +48,8 @@4848 4949 Implementation details live alongside the code:5050 51-- [Cryptographic test vectors](./docs/CRYPTOGRAPHIC-TEST-VECTORS.md)52-- [WebAssembly provenance](./docs/WASM-PROVENANCE.md)53-51+- [Security finding resolutions](./docs/PEACH_BROWSER_SECURITY_FINDING_RESOLUTIONS_2026-09-05.md)52+- [Cross-client Sync protocol](./docs/sync-protocol.md)5453 ---5554 5655 ## Building

view file

manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.2.50",4+  "version": "5.2.51",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.2.50",5+  "version": "5.2.51",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.2.50",3+  "version": "5.2.51",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/message-policy.test.ts+1 −1
@@ -11,7 +11,7 @@1111   it("classifies every declared message type", () => {1212     const policy = MESSAGE_CONTEXT_POLICY as Record<string, string>;1313     const declaredTypes: MessageType[] = [14-      "GET_SYNC_ALERTS", "CLEAR_CLIPBOARD", "GET_PENDING_VOUCHED_DEVICES", "CONFIRM_VOUCHED_DEVICE",14+      "GET_SYNC_ALERTS", "DISMISS_SYNC_ALERTS", "CLEAR_CLIPBOARD", "GET_PENDING_VOUCHED_DEVICES", "CONFIRM_VOUCHED_DEVICE",1515       "REJECT_VOUCHED_DEVICE", "VOUCHED_DEVICE_PENDING", "ROTATE_SYNC_IDENTITY", "DEVICE_KEY_ROTATED",1616       "UNLOCK", "UNLOCK_WITH_PIN", "GET_PIN_STATUS", "LOCK", "IS_UNLOCKED", "HAS_VAULT", "GET_ENTRIES", "EXPORT_ENTRIES", "ADD_ENTRY",1717       "BATCH_ADD_ENTRIES", "RESTORE_ENTRIES", "CLEAR_VAULT_ENTRIES", "UPDATE_ENTRY", "ADD_ADDITIONAL_URL",

view file

src/background/message-policy.ts+1 −1
@@ -17,8 +17,8 @@1717  * fail type-checking instead of silently becoming trusted.1818  */1919 export const MESSAGE_CONTEXT_POLICY = {20-  // Popup/options operations and sensitive reads/writes.2120   GET_SYNC_ALERTS: "extension-page-only",21+  DISMISS_SYNC_ALERTS: "extension-page-only",2222   GET_PENDING_VOUCHED_DEVICES: "extension-page-only",2323   CONFIRM_VOUCHED_DEVICE: "extension-page-only",2424   REJECT_VOUCHED_DEVICE: "extension-page-only",

view file

src/background/messages.ts+12 −8
@@ -10,7 +10,7 @@1010  */1111 1212 import { copyToClipboard, getEffectiveClearSeconds } from "./clipboard";13-import { getPendingSyncAlerts, clearPendingSyncAlerts } from "./sync-alerts";13+import { dismissPendingSyncAlerts, getPendingSyncAlerts } from "./sync-alerts";1414 import { extractDomain } from "../shared/domain";1515 import {1616   getPendingVouchedDevice,@@ -1876,13 +1876,17 @@18761876 18771877       case "GET_SYNC_ALERTS": {18781878         if (vault.isDecoyMode()) return { ok: true, data: [] };1879-        const { consume } = (msg.payload ?? {}) as { consume?: boolean };1880-        const alerts = await getPendingSyncAlerts();1881-        // The popup reads on open and acknowledges with consume:true after1882-        // rendering, so a popup crash between read and render doesn't lose1883-        // the alerts.1884-        if (consume) await clearPendingSyncAlerts();1885-        return { ok: true, data: alerts };1879+        return { ok: true, data: await getPendingSyncAlerts() };1880+      }1881+1882+      case "DISMISS_SYNC_ALERTS": {1883+        if (vault.isDecoyMode()) return { ok: true };1884+        const type = (msg.payload as { type?: unknown } | undefined)?.type;1885+        if (type !== "DEVICE_KEY_ROTATED" && type !== "DEVICE_REVOKED") {1886+          return { ok: false, error: "invalid_sync_alert_type" };1887+        }1888+        await dismissPendingSyncAlerts(type);1889+        return { ok: true };18861890       }18871891 18881892       case "GET_PENDING_VOUCHED_DEVICES": {

view file

src/background/sync-alerts.test.ts+11 −0
@@ -1,6 +1,7 @@11 import { beforeEach, describe, expect, it } from "vitest";22 import {33   clearPendingSyncAlerts,4+  dismissPendingSyncAlerts,45   getPendingSyncAlerts,56   recordSyncAlerts,67 } from "./sync-alerts";@@ -56,5 +57,15 @@5657     ]);5758     await clearPendingSyncAlerts();5859     expect(await getPendingSyncAlerts()).toEqual([]);60+  });61+  it("dismisses one alert group without losing another", async () => {62+    await recordSyncAlerts([63+      { type: "DEVICE_KEY_ROTATED", entryId: "", entryTitle: "", deviceName: "k", timestamp: 1 },64+      { type: "DEVICE_REVOKED", entryId: "", entryTitle: "", deviceName: "r", timestamp: 2 },65+    ]);66+    await dismissPendingSyncAlerts("DEVICE_KEY_ROTATED");67+    expect(await getPendingSyncAlerts()).toEqual([68+      { type: "DEVICE_REVOKED", entryId: "", entryTitle: "", deviceName: "r", timestamp: 2 },69+    ]);5970   });6071 });

view file

src/background/sync-alerts.ts+15 −4
@@ -4,10 +4,9 @@44 // events enter this queue so a reliable sync does not turn the vault into an55 // activity feed.66 //7-// Persistence: chrome.storage.local under a dedicated key so alerts survive87 // MV3 worker eviction between the sync round and the next popup open. The9-// queue is cleared after the popup acknowledges display (GET_SYNC_ALERTS10-// with `consume: true`).8+// queue remains until the popup explicitly dismisses the corresponding alert9+// group.1110 1211 import { capDeviceName } from "../shared/sync-limits";1312 @@ -48,10 +47,22 @@4847 export async function getPendingSyncAlerts(): Promise<SyncAlert[]> {4948   return readAlerts();5049 }51-5250 export async function clearPendingSyncAlerts(): Promise<void> {5351   try {5452     await chrome.storage.local.remove(SYNC_ALERTS_KEY);53+  } catch {54+    /* non-fatal */55+  }56+}57+58+export async function dismissPendingSyncAlerts(type: SyncAlert["type"]): Promise<void> {59+  try {60+    const remaining = (await readAlerts()).filter((alert) => alert.type !== type);61+    if (remaining.length === 0) {62+      await chrome.storage.local.remove(SYNC_ALERTS_KEY);63+    } else {64+      await chrome.storage.local.set({ [SYNC_ALERTS_KEY]: remaining });65+    }5566   } catch {5667     /* non-fatal */5768   }

view file

src/popup/App.tsx+8 −8
@@ -161,14 +161,14 @@161161       ok: boolean;162162       data?: SyncAlert[];163163     };164-    if (r.ok && (r.data?.length ?? 0) > 0) {165-      setSyncAlerts(r.data ?? []);166-      // Acknowledge so a crash between read and render doesn't lose them —167-      // they remain on screen until the user dismisses each.168-      void sendMessage({ type: "GET_SYNC_ALERTS", payload: { consume: true } });169-    }164+    if (r.ok) setSyncAlerts(r.data ?? []);170165   }171-  function dismissAlertGroup(type: string) {166+  async function dismissAlertGroup(type: SyncAlert["type"]) {167+    const result = (await sendMessage({168+      type: "DISMISS_SYNC_ALERTS",169+      payload: { type },170+    })) as { ok: boolean };171+    if (!result.ok) return;172172     setSyncAlerts((arr) => arr.filter((a) => a.type !== type));173173     setAlertFlipIdx((m) => { const n = { ...m }; delete n[type]; return n; });174174   }@@ -1692,7 +1692,7 @@16921692                           class="peach-sync-alert-dismiss"16931693                           type="button"16941694                           aria-label={group.length > 1 ? "Dismiss all" : "Dismiss"}1695-                          onClick={() => dismissAlertGroup(type)}1695+                          onClick={() => void dismissAlertGroup(type as SyncAlert["type"])}16961696                         >×</button>16971697                       </div>16981698                     );

view file

src/shared/release-notes.ts+9 −1
@@ -18,8 +18,16 @@1818   notes: ReleaseNote[];1919   importance?: ReleaseImportance;2020 }21-2221 export const RELEASES: PeachRelease[] = [22+  {23+    version: "5.2.51",24+    releasedAt: "2026-09-11",25+    notes: [{26+      heading: "Keep Sync security alerts until dismissal",27+      body: "Device-key and revocation alerts remain queued until you dismiss them, so closing the popup cannot hide a trust event.",28+    }],29+  },30+2331   {2432     version: "5.2.49",2533     releasedAt: "2026-09-11",

view file

src/shared/types.ts+2 −1
@@ -197,8 +197,9 @@197197 // handled in src/background/messages.ts. Everything in this group must198198 // have a matching case in that file's switch statement.199199 export type MessageType =200-  | "GET_SYNC_ALERTS"201200   | "CLEAR_CLIPBOARD"201+  | "GET_SYNC_ALERTS"202+  | "DISMISS_SYNC_ALERTS"202203   | "GET_PENDING_VOUCHED_DEVICES"203204   | "CONFIRM_VOUCHED_DEVICE"204205   | "REJECT_VOUCHED_DEVICE"

view file