c730b3f3bfe8

reconcile stale Noise-XX comments with the NK reconnect path, default PairedDevice.wsPort to the real 49142 listener, and add the real 7-frame RelayMessage contract from Android's RelayFrames.kt (targetDeviceId is the NK static pubkey hex, not a device-id string)

time
2026-07-25 01:51 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
standardize sync on Noise NK
parent
66a788d0df33
4 files src/background/code-pairing.ts+4 −3src/background/sync-coordinator.ts+3 −3src/shared/pairing-handshake.ts+4 −2src/shared/sync-types.ts+29 −5
src/background/code-pairing.ts+4 −3
@@ -14,13 +14,14 @@1414 // primary path: the phone listens and the code carries its address. A legacy1515 // 6-digit helper code has no address, so the host is probed for.1616 17-import { dialPairingChannel, type PairingDialDeps } from "../shared/pairing-dial";17+import { dialPairingChannel, PAIRING_PORT, type PairingDialDeps } from "../shared/pairing-dial";1818 import type { LocalIdentity } from "../shared/pairing-handshake";1919 import { parsePairingCode } from "../shared/pairing-code";2020 import type { DeviceType, PairedDevice } from "../shared/sync-types";2121 22-/** Default sync port a peer announces; used when ws_port is absent. */23-const DEFAULT_SYNC_PORT = 8765;22+/** Default sync port a peer announces; used when ws_port is absent. Same23+ *  fixed port pairing dials — SyncReconnectListener.SYNC_PORT on Android. */24+const DEFAULT_SYNC_PORT = PAIRING_PORT;2425 2526 export type CodePairingError =2627   | "invalid_code"      // socket closed before PEACH_HELLO_ACK (wrong/expired)

view file

src/background/sync-coordinator.ts+3 −3
@@ -1,8 +1,8 @@11 // Vault sync — Phase 2 manifest-diff coordinator.22 //3-// After Noise XX is open and the responder's static key has been verified4-// against the paired-device registry, this coordinator drives the5-// manifest exchange:3+// After Noise NK is open and PAIR_IDENTITY has been exchanged4+// (`shared/sync-transport.ts`, HANDSHAKE_STANDARDIZE_ON_NK.md), this5+// coordinator drives the manifest exchange:66 //77 //   ->  SYNC_HELLO { vaultId, deviceId }88 //   <-  SYNC_HELLO { vaultId, deviceId }       (mismatch → SYNC_ERROR)

view file

src/shared/pairing-handshake.ts+4 −2
@@ -24,8 +24,10 @@2424 //      persist the counterpart in the paired-device registry.2525 //2626 //   3. The Noise NK session and the ephemerals are discarded — future27-//      WebSocket sessions use Noise XX with the long-term static keys28-//      exchanged in step 2.27+//      WebSocket sessions (`shared/sync-transport.ts`) run Noise NK again,28+//      this time against the long-term static key exchanged in step 2,29+//      followed by a fresh PAIR_IDENTITY exchange (see30+//      HANDSHAKE_STANDARDIZE_ON_NK.md; ongoing sync does NOT use Noise XX).2931 3032 import { noiseNKInitiator, noiseNKResponder } from "./noise-js";3133 import type { DeviceType, PairedDevice } from "./sync-types";

view file

src/shared/sync-types.ts+29 −5
@@ -45,9 +45,10 @@4545  * keys must not leave the device via a third-party cloud service.4646  *4747  * `longTermPublicKeyHex` is the counterpart's vault-derived X25519 sync48- * identity static key — see `deriveSyncIdentityKeypair`. Mutual Noise XX49- * authentication on every WebSocket connection rejects any peer not50- * present in this registry.48+ * identity static key — see `deriveSyncIdentityKeypair`. Every WebSocket49+ * reconnect runs Noise NK against this key (authenticating the responder50+ * only) followed by a PAIR_IDENTITY exchange checked against this registry —51+ * see HANDSHAKE_STANDARDIZE_ON_NK.md.5152  *5253  * `lastKnownIp` is intentionally NOT stored here — it's per-network and5354  * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See@@ -81,9 +82,10 @@8182 // ---------------------------------------------------------------------------8283 8384 /**84- * v3 wire protocol — after Noise XX is open.85+ * v3 wire protocol — after Noise NK is open and PAIR_IDENTITY has been86+ * exchanged (see HANDSHAKE_STANDARDIZE_ON_NK.md).8587  *86- * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise XX. The88+ * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise NK. The8789  * WebSocket transport sees only ciphertext.8890  *8991  * The manifest carries per-entry timestamps; the receiver requests entries@@ -99,6 +101,28 @@99101   | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest }100102   | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }101103   | { type: "SYNC_ERROR"; error: string };104+105+// ---------------------------------------------------------------------------106+// Browser-to-browser relay frames (phone forwards; see peach-android's107+// RelayFrames.kt / RelayHub.kt, the source of truth this mirrors — NOT the108+// older 4-frame PEACH_SYNC_RELAY_DESIGN.md sketch, which is stale).109+//110+// Type-only for now: nothing in this repo sends or handles these yet — no111+// browser can listen, so relaying laptop-to-laptop traffic through a shared112+// phone is unbuilt browser-side work, tracked separately. This exists so113+// that work starts from the real wire contract instead of the stale doc.114+// ---------------------------------------------------------------------------115+116+/** RelayMessage.targetDeviceId/peerDeviceId are the destination's Noise NK117+ *  static public key as lowercase hex — NOT `PairedDevice.deviceId`. */118+export type RelayMessage =119+  | { type: "RELAY_REQUEST"; targetDeviceId: string }120+  | { type: "RELAY_ACCEPT" }121+  | { type: "RELAY_UNAVAILABLE" }122+  | { type: "RELAY_DISABLED" }123+  | { type: "RELAY_OPEN"; peerDeviceId: string }124+  | { type: "RELAY_FRAME"; payload: string }125+  | { type: "RELAY_CLOSE" };102126 103127 // ---------------------------------------------------------------------------104128 // v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)

view file