// Background wiring for code pairing. // // Keeps `code-pairing.ts` a PURE, dependency-injected client (so its unit tests // run without chrome / vault / WebSocket). This module supplies the production // `CodePairingDeps` and the single-flight SAS/confirm round-trip that bridges // the blocking `confirm(sas)` callback to the popup via runtime messages: // // PAIR_WITH_CODE → startCodePairing() (popup → bg, messages.ts) // CODE_PAIR_SAS ← push when onSasReady fires (bg → popup) // CODE_PAIR_CONFIRM → resolveCodePairingConfirm() (popup → bg, messages.ts) // CODE_PAIR_RESULT ← push on complete/error (bg → popup) import { pairWithCode, type CodePairingDeps, type CodePairingCallbacks, } from "./code-pairing"; import { openPairingSocket } from "../shared/pairing-dial"; import { clearDeviceRevocation, upsertPairedDevice, setLastKnownIp, } from "./paired-devices"; import { buildLocalIdentity } from "./device-identity"; import { computeVaultId, deriveSyncIdentityKeypair } from "../shared/sync-identity"; import { isUnlocked, getRootKeyHex } from "./vault"; import type { LocalIdentity } from "../shared/pairing-handshake"; import { getRelaySettings } from "./relay-settings"; function hexToBytesLocal(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return out; } /** Best-effort push to the popup. The popup may be closed — pushes are advisory. */ function push(type: string, payload: unknown): void { try { const maybe = chrome.runtime.sendMessage({ type, payload }) as unknown; if (maybe && typeof (maybe as Promise).catch === "function") { (maybe as Promise).catch(() => undefined); } } catch { // Popup closed / no receiver — fine. } } /** Production dependency set — real WebSocket, identity, and storage. */ export function productionCodePairingDeps(): CodePairingDeps { return { openSocket: (ip, port) => openPairingSocket(ip, port), async getRelayUrl() { return (await getRelaySettings()).relayUrl; }, async getLocalIdentity(): Promise<{ identity: LocalIdentity; privateKey: Uint8Array; publicKey: Uint8Array; }> { if (!isUnlocked()) throw new Error("vault_locked"); const masterKeyHex = getRootKeyHex(); if (!masterKeyHex) throw new Error("vault_locked"); // SEV-2-B: derive from the stable ROOT key so pairing survives a // master-password change. Zero the copy when done. const masterKey = hexToBytesLocal(masterKeyHex); try { const base = await buildLocalIdentity(masterKey); const vaultId = await computeVaultId(masterKey); const keys = deriveSyncIdentityKeypair(masterKey, base.deviceId); return { identity: { ...base, vaultId }, privateKey: keys.secretKey, publicKey: keys.publicKey, }; } finally { masterKey.fill(0); } }, // A direct code pairing means a physically present user confirmed the SAS // on both screens — the strongest consent signal in the system, and the // only thing allowed to lift a terminal revocation (see revokeDeviceById). persistDevice: async (d) => { await clearDeviceRevocation(d.deviceId); await upsertPairedDevice(d); }, cacheLastKnownIp: (pubkeyHex, ip) => setLastKnownIp(pubkeyHex, ip), }; } // ── Single in-flight pairing per worker ────────────────────────────────────── // One pairing at a time. The blocking confirm() resolver is parked here while // the popup shows the SAS; CODE_PAIR_CONFIRM resolves it. let pendingConfirm: ((confirmed: boolean) => void) | null = null; let inFlight = false; function clearPairingState(): void { pendingConfirm = null; inFlight = false; } export function isCodePairingInFlight(): boolean { return inFlight; } /** * Kick off a code pairing. Returns immediately; the SAS and the terminal * result are delivered to the popup as CODE_PAIR_SAS / CODE_PAIR_RESULT * pushes. `deps` is injectable for tests. */ export function startCodePairing( code: string, deps: CodePairingDeps = productionCodePairingDeps(), ): void { inFlight = true; const cb: CodePairingCallbacks = { onSasReady: (sas) => push("CODE_PAIR_SAS", { sas }), onComplete: (device) => { push("CODE_PAIR_RESULT", { status: "complete", device }); clearPairingState(); }, onError: (error) => { push("CODE_PAIR_RESULT", { status: "error", error }); clearPairingState(); }, }; // The confirm callback blocks on the popup's CODE_PAIR_CONFIRM reply. const confirm = (_sas: string): Promise => new Promise((resolve) => { pendingConfirm = resolve; }); void pairWithCode(code, cb, deps, confirm).finally(() => { // onComplete/onError already cleared state; this is the backstop for an // unexpected throw that skipped the callbacks. if (inFlight) clearPairingState(); }); } /** Resolve the parked confirm() with the user's decision from the popup. */ export function resolveCodePairingConfirm(confirmed: boolean): void { const resolve = pendingConfirm; pendingConfirm = null; if (resolve) resolve(confirmed); }