123456789101112131415161718192021222324252627import { noiseNKInitiator, noiseNKResponder } from "./noise-js";28import type { DeviceType, PairedDevice } from "./sync-types";2930export interface PairingTransport {31 send(msg: Uint8Array): Promise<void>;32 receive(): Promise<Uint8Array>;33}3435export interface LocalIdentity {36 deviceId: string;37 deviceName: string;38 deviceType: DeviceType;39 longTermPublicKeyHex: string;40}4142interface PairingFrame {43 type: "PAIR_IDENTITY";44 device_id: string;45 device_name: string;46 device_type: DeviceType;47 long_term_public_key_hex: string;48}4950function encodeFrame(frame: PairingFrame): Uint8Array {51 return new TextEncoder().encode(JSON.stringify(frame));52}5354function decodeFrame(bytes: Uint8Array): PairingFrame {55 const parsed = JSON.parse(new TextDecoder().decode(bytes));56 if (parsed?.type !== "PAIR_IDENTITY") throw new Error("expected PAIR_IDENTITY frame");57 if (typeof parsed.device_id !== "string" || !parsed.device_id) throw new Error("device_id missing");58 if (typeof parsed.device_name !== "string") throw new Error("device_name missing");59 if (typeof parsed.device_type !== "string") throw new Error("device_type missing");60 if (typeof parsed.long_term_public_key_hex !== "string") throw new Error("long_term_public_key_hex missing");61 62 if (!/^[0-9a-f]{64}$/.test(parsed.long_term_public_key_hex)) {63 throw new Error("long_term_public_key_hex is not 32-byte hex");64 }65 return parsed as PairingFrame;66}676869707172737475export async function runPairingInitiator(76 remoteEphemeralPublicKey: Uint8Array,77 ourIdentity: LocalIdentity,78 transport: PairingTransport,79): Promise<PairedDevice> {80 const session = await noiseNKInitiator(81 remoteEphemeralPublicKey,82 transport.send,83 transport.receive,84 );8586 87 const ourFrame = encodeFrame({88 type: "PAIR_IDENTITY",89 device_id: ourIdentity.deviceId,90 device_name: ourIdentity.deviceName,91 device_type: ourIdentity.deviceType,92 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,93 });94 await transport.send(session.encrypt(ourFrame));9596 const theirCipher = await transport.receive();97 const theirPlain = session.decrypt(theirCipher);98 const theirFrame = decodeFrame(theirPlain);99 return frameToPairedDevice(theirFrame);100}101102103104105106107108109export async function runPairingResponder(110 ourEphemeralPrivateKey: Uint8Array,111 ourEphemeralPublicKey: Uint8Array,112 ourIdentity: LocalIdentity,113 transport: PairingTransport,114): Promise<PairedDevice> {115 const session = await noiseNKResponder(116 ourEphemeralPrivateKey,117 ourEphemeralPublicKey,118 transport.send,119 transport.receive,120 );121122 123 const theirCipher = await transport.receive();124 const theirPlain = session.decrypt(theirCipher);125 const theirFrame = decodeFrame(theirPlain);126127 const ourFrame = encodeFrame({128 type: "PAIR_IDENTITY",129 device_id: ourIdentity.deviceId,130 device_name: ourIdentity.deviceName,131 device_type: ourIdentity.deviceType,132 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,133 });134 await transport.send(session.encrypt(ourFrame));135136 return frameToPairedDevice(theirFrame);137}138139function frameToPairedDevice(frame: PairingFrame): PairedDevice {140 return {141 deviceId: frame.device_id,142 deviceName: frame.device_name,143 deviceType: frame.device_type,144 longTermPublicKeyHex: frame.long_term_public_key_hex,145 pairedAt: Date.now(),146 };147}148149150151152153export function memoryTransportPair(): [PairingTransport, PairingTransport] {154 const a: Uint8Array[] = [];155 const b: Uint8Array[] = [];156 const aReaders: Array<(b: Uint8Array) => void> = [];157 const bReaders: Array<(b: Uint8Array) => void> = [];158159 function makeTransport(160 inbox: Uint8Array[],161 outbox: Uint8Array[],162 inboxReaders: Array<(b: Uint8Array) => void>,163 outboxReaders: Array<(b: Uint8Array) => void>,164 ): PairingTransport {165 return {166 async send(msg: Uint8Array) {167 const r = outboxReaders.shift();168 if (r) r(msg);169 else outbox.push(msg);170 },171 receive() {172 const msg = inbox.shift();173 if (msg) return Promise.resolve(msg);174 return new Promise<Uint8Array>((resolve) => inboxReaders.push(resolve));175 },176 };177 }178 return [makeTransport(a, b, aReaders, bReaders), makeTransport(b, a, bReaders, aReaders)];179}