01-vault-id-in-pair-identity.md

6.3 KB · raw

rendered · source

Change 1 — Add vault_id to the PAIR_IDENTITY frame

DRAFT. Conforms to _GROUND-TRUTH.md. Target file: /Users/june/projects/peach-browser/src/shared/chirp-handshake.ts.

Goal

Have the extension send vault_id in the PAIR_IDENTITY frame so the desktop helper can persist which vault it was paired to. peach-sync already accepts vault_id as an optional field (peach-sync/src/pairing.zig:54, tolerated/ignored when absent), so this is purely additive and backwards-compatible.

Backwards-compatibility statement

Privacy consideration (carry the existing note forward)

vault_id is the same stable, LAN-broadcast identifier documented at src/shared/sync-identity.ts:62-74 (PRIVACY NOTE / PCH-02-006): it is stable across sessions and lets a passive LAN observer link every device sharing a vault. Putting it in PAIR_IDENTITY keeps it inside the encrypted NK transport, so this change does not add network exposure beyond what mDNS sync discovery already leaks. The patch comment references that note rather than restating it.

Threading: where vault_id enters the handshake

computeVaultId is async and needs the in-memory masterKey (sync-identity.ts:75-82), neither of which is in scope inside runPairingInitiator/runPairingResponder. The clean seam is the existing LocalIdentity argument already passed into both functions: add an optional vaultId field, computed by the caller that already holds the master key (the pairing-flow entry point in the popup/background that builds LocalIdentity). No master key is introduced into chirp-handshake.ts.


Patch (apply to src/shared/chirp-handshake.ts)

Hunk A — extend LocalIdentity with optional vaultId

Context: lines 43-51.

 export interface LocalIdentity {
   deviceId: string;
   deviceName: string;
   deviceType: DeviceType;
   longTermPublicKeyHex: string;
   signingPublicKeyHex?: string;
   /** Server-only field — Android/iOS/desktop sync hosts announce their port. */
   wsPort?: number;
+  /**
+   * Stable per-vault id (`computeVaultId`, sync-identity.ts). Sent in the
+   * outbound PAIR_IDENTITY so a desktop helper can record which vault it
+   * paired to. Optional: the caller omits it when the vault is locked (no
+   * master key). Privacy: this is the LAN-linkable id — see the PRIVACY NOTE
+   * at sync-identity.ts:62 (PCH-02-006).
+   */
+  vaultId?: string;
 }

Hunk B — add vault_id to the PairingFrame interface

Context: lines 53-61.

 interface PairingFrame {
   type: "PAIR_IDENTITY";
   device_id: string;
   device_name: string;
   device_type: DeviceType;
   long_term_public_key_hex: string;
   signing_public_key_hex?: string;
   ws_port?: number;
+  // Optional: present only when the sender knows its vault id. peach-sync
+  // treats this as optional (pairing.zig:54); chirp mobile peers omit it.
+  vault_id?: string;
 }

Hunk C — emit vault_id from the INITIATOR frame

Context: lines 106-114 (runPairingInitiator).

   const ourFrame = encodeFrame({
     type: "PAIR_IDENTITY",
     device_id: ourIdentity.deviceId,
     device_name: ourIdentity.deviceName,
     device_type: ourIdentity.deviceType,
     long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,
     signing_public_key_hex: ourIdentity.signingPublicKeyHex,
     ws_port: ourIdentity.wsPort,
+    vault_id: ourIdentity.vaultId,
   });

Hunk D — emit vault_id from the RESPONDER frame

Context: lines 209-217 (runPairingResponder).

   const ourFrame = encodeFrame({
     type: "PAIR_IDENTITY",
     device_id: ourIdentity.deviceId,
     device_name: ourIdentity.deviceName,
     device_type: ourIdentity.deviceType,
     long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,
     signing_public_key_hex: ourIdentity.signingPublicKeyHex,
     ws_port: ourIdentity.wsPort,
+    vault_id: ourIdentity.vaultId,
   });

encodeFrame is JSON.stringify (line 63-65): when vaultId is undefined, JSON.stringify drops the key entirely, so the omitted-field backwards-compat behaviour is automatic — no extra guard needed.

(Optional) Hunk E — validate vault_id on decode if present

decodeFrame (lines 67-85) currently ignores extra fields. The helper never sends vault_id, so receive-side validation is not required for desktop pairing. If a future symmetric flow has the peer send it, add a lenient guard:

   if (
     parsed.signing_public_key_hex !== undefined &&
     !/^[0-9a-f]{64}$/.test(parsed.signing_public_key_hex)
   ) {
     throw new Error("signing_public_key_hex is not 32-byte hex");
   }
+  // vault_id, when present, is a 64-char lowercase hex BLAKE3 digest
+  // (computeVaultId). Lenient: only validate shape if the peer sent one.
+  if (parsed.vault_id !== undefined && !/^[0-9a-f]{64}$/.test(parsed.vault_id)) {
+    throw new Error("vault_id is not 32-byte hex");
+  }
   return parsed as PairingFrame;

Recommendation: ship Hunks A–D only for the desktop-pairing milestone; defer Hunk E until a peer actually sends vault_id, to avoid over-constraining.

Notes for the implementer