12345678910111213141516171819202122232425262728import { noiseNKInitiator, noiseNKResponder } from "./noise-js";29import type { DeviceType, PairedDevice } from "./sync-types";3031export interface PairingTransport {32 send(msg: Uint8Array): Promise<void>;33 receive(): Promise<Uint8Array>;34}3536export interface LocalIdentity {37 deviceId: string;38 deviceName: string;39 deviceType: DeviceType;40 longTermPublicKeyHex: string;41 signingPublicKeyHex?: string;42 43 wsPort?: number;44}4546interface PairingFrame {47 type: "PAIR_IDENTITY";48 device_id: string;49 device_name: string;50 device_type: DeviceType;51 long_term_public_key_hex: string;52 signing_public_key_hex?: string;53 ws_port?: number;54}5556function encodeFrame(frame: PairingFrame): Uint8Array {57 return new TextEncoder().encode(JSON.stringify(frame));58}5960function decodeFrame(bytes: Uint8Array): PairingFrame {61 const parsed = JSON.parse(new TextDecoder().decode(bytes));62 if (parsed?.type !== "PAIR_IDENTITY") throw new Error("expected PAIR_IDENTITY frame");63 if (typeof parsed.device_id !== "string" || !parsed.device_id) throw new Error("device_id missing");64 if (typeof parsed.device_name !== "string") throw new Error("device_name missing");65 if (typeof parsed.device_type !== "string") throw new Error("device_type missing");66 if (typeof parsed.long_term_public_key_hex !== "string") throw new Error("long_term_public_key_hex missing");67 68 if (!/^[0-9a-f]{64}$/.test(parsed.long_term_public_key_hex)) {69 throw new Error("long_term_public_key_hex is not 32-byte hex");70 }71 if (72 parsed.signing_public_key_hex !== undefined &&73 !/^[0-9a-f]{64}$/.test(parsed.signing_public_key_hex)74 ) {75 throw new Error("signing_public_key_hex is not 32-byte hex");76 }77 return parsed as PairingFrame;78}798081828384858687export async function runPairingInitiator(88 remoteEphemeralPublicKey: Uint8Array,89 ourIdentity: LocalIdentity,90 transport: PairingTransport,91): Promise<PairedDevice> {92 const session = await noiseNKInitiator(93 remoteEphemeralPublicKey,94 transport.send,95 transport.receive,96 );9798 99 const ourFrame = encodeFrame({100 type: "PAIR_IDENTITY",101 device_id: ourIdentity.deviceId,102 device_name: ourIdentity.deviceName,103 device_type: ourIdentity.deviceType,104 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,105 signing_public_key_hex: ourIdentity.signingPublicKeyHex,106 ws_port: ourIdentity.wsPort,107 });108 await transport.send(session.encrypt(ourFrame));109110 const theirCipher = await transport.receive();111 const theirPlain = session.decrypt(theirCipher);112 const theirFrame = decodeFrame(theirPlain);113 return frameToPairedDevice(theirFrame);114}115116117118119120121122123export async function runPairingResponder(124 ourEphemeralPrivateKey: Uint8Array,125 ourEphemeralPublicKey: Uint8Array,126 ourIdentity: LocalIdentity,127 transport: PairingTransport,128): Promise<PairedDevice> {129 const session = await noiseNKResponder(130 ourEphemeralPrivateKey,131 ourEphemeralPublicKey,132 transport.send,133 transport.receive,134 );135136 137 const theirCipher = await transport.receive();138 const theirPlain = session.decrypt(theirCipher);139 const theirFrame = decodeFrame(theirPlain);140141 const ourFrame = encodeFrame({142 type: "PAIR_IDENTITY",143 device_id: ourIdentity.deviceId,144 device_name: ourIdentity.deviceName,145 device_type: ourIdentity.deviceType,146 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,147 signing_public_key_hex: ourIdentity.signingPublicKeyHex,148 ws_port: ourIdentity.wsPort,149 });150 await transport.send(session.encrypt(ourFrame));151152 return frameToPairedDevice(theirFrame);153}154155function frameToPairedDevice(frame: PairingFrame): PairedDevice {156 return {157 deviceId: frame.device_id,158 deviceName: frame.device_name,159 deviceType: frame.device_type,160 longTermPublicKeyHex: frame.long_term_public_key_hex,161 signingPublicKeyHex: frame.signing_public_key_hex,162 trustSource: "direct",163 wsPort: typeof frame.ws_port === "number" ? frame.ws_port : undefined,164 pairedAt: Date.now(),165 };166}167168169170171172export function memoryTransportPair(): [PairingTransport, PairingTransport] {173 const a: Uint8Array[] = [];174 const b: Uint8Array[] = [];175 const aReaders: Array<(b: Uint8Array) => void> = [];176 const bReaders: Array<(b: Uint8Array) => void> = [];177178 function makeTransport(179 inbox: Uint8Array[],180 outbox: Uint8Array[],181 inboxReaders: Array<(b: Uint8Array) => void>,182 outboxReaders: Array<(b: Uint8Array) => void>,183 ): PairingTransport {184 return {185 async send(msg: Uint8Array) {186 const r = outboxReaders.shift();187 if (r) r(msg);188 else outbox.push(msg);189 },190 receive() {191 const msg = inbox.shift();192 if (msg) return Promise.resolve(msg);193 return new Promise<Uint8Array>((resolve) => inboxReaders.push(resolve));194 },195 };196 }197 return [makeTransport(a, b, aReaders, bReaders), makeTransport(b, a, bReaders, aReaders)];198}