c62fe9f5c847
pin that each reconnect derives fresh session keys, and record why intra-session rekeying buys nothing on a one-shot session (v4.1.8)
- time
- 2026-07-26 08:55 UTC · 3 months ago
- machine
- koh2
- agent
- claude-opus-5
- session
- sync campaign remaining items
- parent
- 22016a5dbc67
↓ peach-browser-c62fe9f5.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/shared/noise-kk.test.ts+45 −0src/shared/sync-transport.ts+15 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.1.7",4+ "version": "4.1.8",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.1.7",4+ "version": "4.1.8",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.1.7",3+ "version": "4.1.8",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/shared/noise-kk.test.ts+45 −0
@@ -48,6 +48,51 @@4848 expect(new TextDecoder().decode(a.decrypt(b.encrypt(new TextEncoder().encode("two"))))).toBe("two");4949 });5050 51+ it("E6: every reconnect derives fresh keys, so one session never exposes another", async () => {52+ // Peach sync sessions are one-shot: ~10 frames, a 30s coordinator timeout,53+ // then close. There is no long-lived session to rekey inside, and each54+ // reconnect runs a full KK handshake with fresh ephemerals — which gives55+ // strictly stronger post-compromise security than intra-session Rekey()56+ // would. This pins that property so a future change cannot quietly reuse57+ // ephemerals and turn every past session's traffic into one key's problem.58+ const initiator = generateKeyPair();59+ const responder = generateKeyPair();60+61+ async function handshake() {62+ const wire = channels();63+ const [a, b] = await Promise.all([64+ noiseKKInitiator(65+ initiator.secretKey,66+ initiator.publicKey,67+ responder.publicKey,68+ wire.initiatorSend,69+ wire.initiatorReceive,70+ ),71+ noiseKKResponder(72+ responder.secretKey,73+ responder.publicKey,74+ initiator.publicKey,75+ wire.responderSend,76+ wire.responderReceive,77+ ),78+ ]);79+ return { a, b };80+ }81+82+ const first = await handshake();83+ const second = await handshake();84+85+ // Same long-term statics, different session — the transcripts must differ.86+ expect(bytesToHex(first.a.handshakeHash)).not.toBe(87+ bytesToHex(second.a.handshakeHash),88+ );89+90+ // And the keys must genuinely differ: a frame from session two must not be91+ // readable by session one.92+ const frame = second.a.encrypt(new TextEncoder().encode("session two"));93+ expect(() => first.b.decrypt(frame)).toThrow();94+ });95+5196 it("pins the cross-platform deterministic handshake hash", async () => {5297 const initiator = generateKeyPairFromSeed(new Uint8Array(32).fill(0x01));5398 const responder = generateKeyPairFromSeed(new Uint8Array(32).fill(0x02));src/shared/sync-transport.ts+15 −0
@@ -27,6 +27,21 @@2727 import { buildRendezvousUrl } from "./rendezvous";2828 import type { DeviceType } from "./sync-types";2929 30+// E6 (session rekeying) is deliberately NOT wired, and this is a reality check31+// against the spec rather than an omission. E6 targets "long-lived sync32+// sessions"; Peach has none. A session is one-shot — roughly ten frames each33+// way, a 30s coordinator timeout (SESSION_TIMEOUT_MS), then close — and every34+// reconnect runs a fresh Noise KK handshake with fresh ephemerals. That already35+// gives stronger post-compromise security than intra-session Rekey() would,36+// because no two sessions share key material at all (pinned by the E6 test in37+// noise-kk.test.ts).38+//39+// Wiring Rekey() anyway would mean both ends rekeying at the identical frame40+// index with no wire signal to resynchronise on. One frame of drift between41+// browser and Android breaks all sync — the exact failure class this protocol42+// has already been burned by twice. The primitives are exposed on NoiseSession43+// (rekeyOutgoing/rekeyIncoming) if a genuinely long-lived session ever appears;44+// until then the cost is real and the benefit is zero.3045 const DEFAULT_PROBE_TIMEOUT_MS = 500;3146 const DEFAULT_PROBE_BATCH = 16;3247 const DEFAULT_CONNECT_TIMEOUT_MS = 4_000;