081ae8b38113
fix: preserve terminal revocations against peer floods and retain local revoke capacity (PB-26-110, v5.0.40)
- time
- 2026-09-01 22:24 UTC · 1 month ago
- machine
- koh2
- agent
- claude-opus-5
- session
- 2026-09-01 security assessment remediation
- parent
- 65263d57b9a4
↓ peach-browser-081ae8b3.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/paired-devices.test.ts+20 −0src/background/paired-devices.ts+13 −3manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.0.39",4+ "version": "5.0.40",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.39",5+ "version": "5.0.40",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.39",3+ "version": "5.0.40",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/paired-devices.test.ts+20 −0
@@ -238,6 +238,26 @@238238 expect(await listDeviceRevocations()).toHaveLength(2);239239 });240240 241+ it("a peer flood cannot evict an older revocation from another issuer", async () => {242+ await mergeDeviceRevocations([{243+ deviceId: "genuine",244+ longTermPublicKeyHex: "a".repeat(64),245+ revokedAt: 1,246+ revokedByDeviceId: "honest-issuer",247+ }]);248+ const flood = Array.from({ length: 600 }, (_, index) => ({249+ deviceId: `junk-${index}`,250+ longTermPublicKeyHex: index.toString(16).padStart(64, "0"),251+ revokedAt: index + 2,252+ revokedByDeviceId: "flooding-issuer",253+ }));254+ const admitted = await mergeDeviceRevocations(flood);255+ expect(admitted).toHaveLength(499);256+ expect(await listDeviceRevocations()).toHaveLength(500);257+ expect(await isDeviceRevoked("genuine")).toBe(true);258+ expect(await isDeviceRevoked("junk-599")).toBe(false);259+ });260+241261 it("a later revokedAt cannot displace an existing record — revocation is terminal", async () => {242262 await mergeDeviceRevocations([243263 { deviceId: "a", longTermPublicKeyHex: "a".repeat(64), revokedAt: 1, revokedByDeviceId: "x" },src/background/paired-devices.ts+13 −3
@@ -206,7 +206,13 @@206206 revokedAt: options.now ?? Date.now(),207207 revokedByDeviceId,208208 };209- await storeRevocations([...existing, record]);209+ const removeIndex = existing.length >= MAX_REVOCATIONS210+ ? Math.max(0, existing.findIndex((r) => r.revokedByDeviceId !== revokedByDeviceId))211+ : -1;212+ const retained = removeIndex >= 0213+ ? existing.filter((_, index) => index !== removeIndex)214+ : existing;215+ await storeRevocations([...retained, record]);210216 await removePairedDeviceById(deviceId);211217 return record;212218 }@@ -231,8 +237,12 @@231237 // terminal, not this clamp (HOLE 2 audit in sync-limits.ts).232238 .map((r) => ({ ...r, revokedAt: clampSyncTimestamp(r.revokedAt) }));233239 if (fresh.length === 0) return [];234- await storeRevocations([...existing, ...fresh].slice(-MAX_REVOCATIONS));235- return fresh.map((r) => r.deviceId);240+ // Peer manifests may add records but never evict terminal records already241+ // held locally. A full ledger drops only newly learned records; the local242+ // revoke path remains responsible for administrative revocations.243+ const admitted = fresh.slice(0, Math.max(0, MAX_REVOCATIONS - existing.length));244+ if (admitted.length > 0) await storeRevocations([...existing, ...admitted]);245+ return admitted.map((r) => r.deviceId);236246 }237247 238248 /**