41b6448ed641
refactor: delete dead GET_ADOPTION_MATERIAL/COMPLETE_PAIRING handlers (v4.8.5)
- time
- 2026-08-04 15:10 UTC · 2 months ago
- machine
- koh2
- parent
- ae66726c3c86
↓ peach-browser-41b6448e.tar.gzatlas at this save
files changed6 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 −2manifest.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": {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",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",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 () => {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": {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"