41b6448ed641

refactor: delete dead GET_ADOPTION_MATERIAL/COMPLETE_PAIRING handlers (v4.8.5)

Follow-up to the 2026-08-04 Codex Security fixes (v4.8.2): the two handlers had zero legitimate senders anywhere in the repo — adoption runs entirely in-background via relay-adoption-bg.ts (direct vault/paired-devices imports), and the Message union made any leftover reference a compile error (tsc clean). Deletion safety verified: - Revocation-lift semantics remain live via repairPairedRevocationOverlaps(), which runs at service-worker boot (entry.ts) and is pinned by paired-devices.test.ts — same 'fresh direct pair clears stale revocation' contract the old handler implemented. - Sync/pairing/mesh regression suites: 11 files / 128 tests green (coordinator, runner, trigger, vault-adapter, static, alarm, settings, mesh-relay-listener, relay-adoption-bg, device-identity, duress-adoption). Removed: both router cases, MessageTypes entries, the handler-only computeVaultId/clearDeviceRevocation imports, and the test that dispatched COMPLETE_PAIRING. PRIVILEGED_MESSAGE_TYPES retains the four destructive types + COMPLETE_FIREFOX_PASSKEY_CREATE.

time
2026-08-04 15:10 UTC · 2 months ago
machine
koh2
parent
ae66726c3c86
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.test.ts+12 −58src/background/messages.ts+6 −74src/shared/types.ts+0 −2
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.8.4",4+  "version": "4.8.5",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": "4.8.4",5+  "version": "4.8.5",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": "4.8.4",3+  "version": "4.8.5",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/messages.test.ts+12 −58
@@ -1207,40 +1207,11 @@12071207     expect(res).toMatchObject({ ok: false, error: "invalid_payload" });12081208   });12091209 1210-  it("a fresh direct re-pair clears the old revocation before restoring the row", async () => {1211-    const vault = await import("./vault");1212-    await vault.createVault("correct-pass-12345");1213-    const {1214-      findPairedDeviceById,1215-      isDeviceRevoked,1216-      upsertPairedDevice,1217-    } = await import("./paired-devices");1218-    const phone = {1219-      deviceId: "phone-1",1220-      deviceName: "June's Phone",1221-      deviceType: "android" as const,1222-      longTermPublicKeyHex: "a".repeat(64),1223-      signingPublicKeyHex: "b".repeat(64),1224-      pairedAt: 1_700_000_000_000,1225-      trustSource: "direct" as const,1226-    };1227-    await upsertPairedDevice(phone);1228-    await dispatch({1229-      type: "UNPAIR_DEVICE",1230-      payload: { deviceId: phone.deviceId },1231-    });1232-    expect(await isDeviceRevoked(phone.deviceId, phone.longTermPublicKeyHex)).toBe(true);1233-1234-    await expect(dispatch({1235-      type: "COMPLETE_PAIRING",1236-      payload: { paired: phone },1237-    }, { id: "peach-extension", url: "chrome-extension://peach-extension/popup.html" }))1238-      .resolves.toMatchObject({ ok: true });1239-1240-    expect(await isDeviceRevoked(phone.deviceId, phone.longTermPublicKeyHex)).toBe(false);1241-    expect(await findPairedDeviceById(phone.deviceId)).toMatchObject(phone);1242-  });1243-1210+  // The old "fresh direct re-pair clears the old revocation" test dispatched1211+  // COMPLETE_PAIRING, a handler deleted on 2026-08-04 (no legitimate senders;1212+  // adoption runs in-background via relay-adoption-bg.ts). The live1213+  // revocation-overlap repair is pinned in paired-devices.test.ts1214+  // (repairPairedRevocationOverlaps, runs at service-worker boot).12441215   it("F1: privileged handlers reject content-script senders but keep extension-page senders (Codex Security 2026-08-04)", async () => {12451216     const vault = await import("./vault");12461217     await vault.createVault("correct-pass-12345");@@ -1252,35 +1223,18 @@12521223       id: "peach-extension",12531224       url: "chrome-extension://peach-extension/popup.html",12541225     };1255-    const paired = {1256-      deviceId: "attacker-1",1257-      deviceName: "Attacker Device",1258-      deviceType: "android" as const,1259-      longTermPublicKeyHex: "c".repeat(64),1260-      signingPublicKeyHex: "d".repeat(64),1261-      pairedAt: 1_700_000_000_000,1262-      trustSource: "direct" as const,1263-    };1226+    const completion = { requestId: 1, rpId: "evil.example" };12641227 1265-    // Content-script senders must never reach key-material or pairing handlers.1228+    // Content-script senders must never reach the gated passkey-store handler.12661229     await expect(1267-      dispatch({ type: "GET_ADOPTION_MATERIAL" }, contentScriptSender)1230+      dispatch({ type: "COMPLETE_FIREFOX_PASSKEY_CREATE", payload: completion }, contentScriptSender)12681231     ).resolves.toMatchObject({ ok: false, error: "invalid_sender" });1269-    await expect(1270-      dispatch({ type: "COMPLETE_PAIRING", payload: { paired } }, contentScriptSender)1271-    ).resolves.toMatchObject({ ok: false, error: "invalid_sender" });1272-    await expect(1273-      dispatch({ type: "COMPLETE_FIREFOX_PASSKEY_CREATE", payload: { requestId: 1, rpId: "evil.example" } }, contentScriptSender)1274-    ).resolves.toMatchObject({ ok: false, error: "invalid_sender" });12751232 1276-    // The paired-device row must NOT have been registered by the rejected call.1277-    const { findPairedDeviceById } = await import("./paired-devices");1278-    expect(await findPairedDeviceById(paired.deviceId)).toBeNull();1279-1280-    // Extension-page senders still reach GET_ADOPTION_MATERIAL.1233+    // Extension-page senders still reach it — refused by the pending-ceremony1234+    // binding, not by the sender gate.12811235     await expect(1282-      dispatch({ type: "GET_ADOPTION_MATERIAL" }, extensionPageSender)1283-    ).resolves.toMatchObject({ ok: true });1236+      dispatch({ type: "COMPLETE_FIREFOX_PASSKEY_CREATE", payload: completion }, extensionPageSender)1237+    ).resolves.toMatchObject({ ok: false, error: "no_pending_request" });12841238   });12851239 12861240   it("F2: COMPLETE_FIREFOX_PASSKEY_CREATE must match the pending ceremony (Codex Security 2026-08-04)", async () => {

view file

src/background/messages.ts+6 −74
@@ -12,7 +12,6 @@1212 import { copyToClipboard, getEffectiveClearSeconds } from "./clipboard";1313 import { getPendingSyncAlerts, clearPendingSyncAlerts } from "./sync-alerts";1414 import { extractDomain } from "../shared/domain";15-import { computeVaultId } from "../shared/sync-identity";1615 import {1716   getPendingVouchedDevice,1817   listPendingVouchedDevices,@@ -73,7 +72,6 @@7372   setDeviceName as setDeviceNameV3,7473 } from "./device-identity";7574 import {76-  clearDeviceRevocation,7775   isDeviceRevoked,7876   listPairedDevices as listPairedDevicesV3,7977   revokeDeviceById as revokeDeviceByIdV3,@@ -161,25 +159,17 @@161159 162160 // ---------------------------------------------------------------------------163161 // PCH-01-018: operations that irreversibly destroy or replace vault contents,164-// plus handlers that export key material or register paired devices. These are165-// gated to trusted extension-page senders in handleMessage; the confirmation166-// UX otherwise lives only in the popup. Keep this in sync when adding new167-// destructive or sensitive handlers.168-// Codex Security 2026-08-04 (HIGH): GET_ADOPTION_MATERIAL (returns the root169-// key) and COMPLETE_PAIRING (registers a device, lifts its revocation, and170-// triggers sync) were reachable from any content script on isUnlocked() alone;171-// COMPLETE_FIREFOX_PASSKEY_CREATE (MEDIUM) stored a passkey for a172-// caller-chosen rpId with no ceremony binding. All three now require a173-// trusted extension-page sender; GET_ADOPTION_MATERIAL/COMPLETE_PAIRING have174-// no legitimate runtime senders (adoption runs in-background via175-// relay-adoption-bg.ts) and are deletion candidates post-release.162+// plus handlers that store passkey material. These are gated to trusted163+// extension-page senders in handleMessage; the confirmation UX otherwise164+// lives only in the popup. Keep this in sync when adding new destructive or165+// sensitive handlers. (Codex Security 2026-08-04: GET_ADOPTION_MATERIAL and166+// COMPLETE_PAIRING were gated here, then deleted outright — they had no167+// legitimate senders; adoption runs in-background via relay-adoption-bg.ts.)176168 const PRIVILEGED_MESSAGE_TYPES: ReadonlySet<string> = new Set([177169   "DESTROY_VAULT",178170   "CLEAR_VAULT_ENTRIES",179171   "RESTORE_VAULT_SNAPSHOT",180172   "RESTORE_FROM_CODEX",181-  "GET_ADOPTION_MATERIAL",182-  "COMPLETE_PAIRING",183173   "COMPLETE_FIREFOX_PASSKEY_CREATE",184174 ]);185175 @@ -1378,64 +1368,6 @@13781368         } finally {13791369           masterKey.fill(0);13801370         }1381-      }1382-1383-      case "GET_ADOPTION_MATERIAL": {1384-        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1385-        const rootKeyHex = vault.getRootKeyHex();1386-        if (!rootKeyHex) return { ok: false, error: "vault_locked" };1387-        const rootKeyBytes = unhexBytes(rootKeyHex);1388-        try {1389-          const data: { rootKeyHex: string; vaultId: string; license?: string } = {1390-            rootKeyHex,1391-            vaultId: await computeVaultId(rootKeyBytes),1392-          };1393-          if (await isProUser()) {1394-            const license = await getLicenseKey();1395-            if (license) data.license = license;1396-          }1397-          return { ok: true, data };1398-        } finally {1399-          rootKeyBytes.fill(0);1400-        }1401-      }1402-1403-      case "COMPLETE_PAIRING": {1404-        // Not Pro-gated — see GET_LOCAL_IDENTITY.1405-        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };1406-        const paired = (msg.payload as { paired?: unknown })?.paired;1407-        if (typeof paired !== "object" || paired === null) {1408-          return { ok: false, error: "invalid_payload" };1409-        }1410-        const candidate = paired as Record<string, unknown>;1411-        if (1412-          typeof candidate.deviceId !== "string" || !candidate.deviceId ||1413-          typeof candidate.deviceName !== "string" || !candidate.deviceName ||1414-          typeof candidate.deviceType !== "string" ||1415-          typeof candidate.longTermPublicKeyHex !== "string" ||1416-          !/^[0-9a-f]{64}$/.test(candidate.longTermPublicKeyHex) ||1417-          (1418-            candidate.signingPublicKeyHex !== undefined &&1419-            (1420-              typeof candidate.signingPublicKeyHex !== "string" ||1421-              !/^[0-9a-f]{64}$/.test(candidate.signingPublicKeyHex)1422-            )1423-          ) ||1424-          typeof candidate.pairedAt !== "number"1425-        ) {1426-          return { ok: false, error: "invalid_paired_device" };1427-        }1428-        // A fresh SAS-confirmed direct pairing is the one operation allowed to1429-        // lift an old terminal revocation. Without this, re-pairing recreated1430-        // an impossible state where the same device was both paired and1431-        // revoked, and a later Remove became a permanent no-op.1432-        await clearDeviceRevocation(candidate.deviceId);1433-        await upsertPairedDeviceV3(candidate as unknown as import("../shared/sync-types").PairedDevice);1434-        // Newly paired device → kick a sync so it gets its first round1435-        // without waiting for the next 10-minute alarm tick. Fire-and-forget;1436-        // the in-flight guard handles overlap with any other trigger.1437-        void triggerSyncIfReachable({ reason: "device_list_changed" }).catch(() => undefined);1438-        return { ok: true };14391371       }14401372 14411373       case "LIST_PAIRED_DEVICES": {

view file

src/shared/types.ts+0 −2
@@ -298,8 +298,6 @@298298   | "CHANGE_PASSWORD"299299   | "GET_SITE_PASSWORD_POLICY"300300   | "GET_LOCAL_IDENTITY"301-  | "GET_ADOPTION_MATERIAL"302-  | "COMPLETE_PAIRING"303301   | "LIST_PAIRED_DEVICES"304302   | "UNPAIR_DEVICE"305303   | "RENAME_LOCAL_DEVICE"

view file