47f10bb9ac6a
fix: fall back to derived signing key when stored issuer key is stale (v4.6.7)
- time
- 2026-07-31 18:17 UTC · 2 months ago
- machine
- koh2
- parent
- b56583e1a012
↓ peach-browser-47f10bb9.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/sync-vault-adapter.test.ts+53 −0src/background/sync-vault-adapter.ts+17 −1manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.6.6",4+ "version": "4.6.7",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.6.6",5+ "version": "4.6.7",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.6.6",3+ "version": "4.6.7",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/sync-vault-adapter.test.ts+53 −0
@@ -830,6 +830,59 @@830830 expect(await findPairedDeviceById("forged-add")).toBeNull();831831 });832832 833+ it("falls back to derived signing key when stored issuer key is stale", async () => {834+ const { vault, adapter } = await setupVault();835+ const { buildSignedDeviceManifest } = await import("../shared/device-manifest");836+ const { deriveSyncSigningKeypair, pubkeyToHex } = await import("../shared/sync-identity");837+ const { findPairedDeviceById, upsertPairedDevice } = await import("./paired-devices");838+ const { listPendingVouchedDevices } = await import("./pending-vouched-devices");839+840+ const issuerId = "stale-key-android";841+ const rootHex = vault.getRootKeyHex()!;842+ const root = Uint8Array.from(rootHex.match(/../g)!.map((b) => parseInt(b, 16)));843+ const issuerKp = await deriveSyncSigningKeypair(root, issuerId);844+845+ await upsertPairedDevice({846+ deviceId: issuerId,847+ deviceName: "Pixel",848+ deviceType: "android",849+ longTermPublicKeyHex: "c".repeat(64),850+ // Stale: an older build persisted this at first-pair under a now-bad851+ // seed. The fallback must recover from this without trusting the row.852+ signingPublicKeyHex: "ff".repeat(32),853+ pairedAt: 1_700_000_000_000,854+ trustSource: "direct",855+ });856+857+ const manifest = await buildSignedDeviceManifest(858+ issuerId,859+ [{860+ deviceId: "onward-peer",861+ deviceName: "Forwarded iPad",862+ deviceType: "extension",863+ longTermPublicKeyHex: "e".repeat(64),864+ signingPublicKeyHex: pubkeyToHex(865+ (await deriveSyncSigningKeypair(root, "onward-peer")).publicKeyRaw,866+ ),867+ relayUrl: MESH_RELAY,868+ pairedAt: 1_700_000_000_010,869+ }],870+ issuerKp.privateKey,871+ );872+873+ await adapter.applyIncomingDeviceManifest?.(874+ manifest,875+ new Uint8Array(32).fill(0xcc),876+ );877+878+ expect(await findPairedDeviceById("onward-peer")).toMatchObject({879+ trustSource: "vouched",880+ relayUrl: MESH_RELAY,881+ rendezvousSecretHex: expect.stringMatching(/^[0-9a-f]{64}$/),882+ });883+ expect(await listPendingVouchedDevices()).toEqual([]);884+ });885+833886 it("S1: a manifest signed by one paired peer is refused over a session authenticated as another", async () => {834887 const { adapter } = await setupVault();835888 const { buildSignedDeviceManifest } = await import("../shared/device-manifest");src/background/sync-vault-adapter.ts+17 −1
@@ -300,7 +300,23 @@300300 hexToPubkey(issuer.signingPublicKeyHex),301301 );302302 } catch {303- return;303+ // Stored key may be stale (older builds persisted the key derived at304+ // first-pair and never refreshed it). The signing keypair is deterministic305+ // from (masterKey, deviceId) on both sides, so retry once with the locally306+ // re-derived pubkey. A genuinely forged signature throws here and is307+ // silently rejected — only a stale-but-correct key survives the retry.308+ const masterForIssuer = requireMasterKey();309+ try {310+ const derived = await deriveSyncSigningKeypair(311+ masterForIssuer,312+ issuer.deviceId,313+ );314+ body = await verifyDeviceManifest(manifest, derived.publicKeyRaw);315+ } catch {316+ return;317+ } finally {318+ masterForIssuer.fill(0);319+ }304320 }305321 if (body.issuerDeviceId !== issuer.deviceId) return;306322