pending-vouched-devices.ts

3.3 KB · raw

1// Pending vouched devices — audit fix MV-7.2//3// A verified extension-signed device manifest may introduce a device this4// install has never seen. Previously such devices were upserted straight5// into the paired-devices registry (trustSource:"vouched") with no user6// confirmation — making the extension a single point of total compromise7// for the device graph. Now new vouched devices land HERE, sync-inert,8// until the user confirms them in the popup (CONFIRM_VOUCHED_DEVICE) or9// rejects them (REJECT_VOUCHED_DEVICE).10//11// Storage: chrome.storage.local, same locality rules as paired-devices.ts —12// these records contain public keys and must not roam through a third-party13// sync back-end.1415import { capDeviceName } from "../shared/sync-limits";1617export interface PendingVouchedDevice {18  deviceId: string;19  deviceName: string;20  deviceType: string;21  longTermPublicKeyHex: string;22  signingPublicKeyHex: string;23  wsPort?: number;24  /** pairedAt claimed by the vouching manifest — carried through on confirm. */25  pairedAt: number;26  receivedAt: number;27  /** deviceId of the extension whose signed manifest vouched for this device. */28  vouchedByDeviceId: string;29}3031const PENDING_KEY = "peach_pending_vouched_devices_v1";32const MAX_PENDING = 50;3334export async function listPendingVouchedDevices(): Promise<PendingVouchedDevice[]> {35  try {36    const result = await chrome.storage.local.get(PENDING_KEY);37    const value = (result as Record<string, unknown>)[PENDING_KEY];38    if (!Array.isArray(value)) return [];39    return value.filter(isPendingVouchedDevice);40  } catch {41    return [];42  }43}4445/** Add or refresh a pending device (keyed by deviceId). Bounded queue. */46export async function addPendingVouchedDevice(47  device: PendingVouchedDevice,48): Promise<void> {49  // Vouch requests carry a peer-chosen name; bound it like every other50  // inbound name so the pending queue cannot be grown without limit.51  device = { ...device, deviceName: capDeviceName(device.deviceName) };52  const pending = await listPendingVouchedDevices();53  const next = pending.filter((d) => d.deviceId !== device.deviceId);54  next.push(device);55  await chrome.storage.local.set({ [PENDING_KEY]: next.slice(-MAX_PENDING) });56}5758export async function getPendingVouchedDevice(59  deviceId: string,60): Promise<PendingVouchedDevice | null> {61  const pending = await listPendingVouchedDevices();62  return pending.find((d) => d.deviceId === deviceId) ?? null;63}6465export async function removePendingVouchedDevice(deviceId: string): Promise<void> {66  const pending = await listPendingVouchedDevices();67  await chrome.storage.local.set({68    [PENDING_KEY]: pending.filter((d) => d.deviceId !== deviceId),69  });70}7172function isPendingVouchedDevice(x: unknown): x is PendingVouchedDevice {73  if (typeof x !== "object" || x === null) return false;74  const r = x as Record<string, unknown>;75  return (76    typeof r.deviceId === "string" && !!r.deviceId &&77    typeof r.deviceName === "string" &&78    typeof r.deviceType === "string" &&79    typeof r.longTermPublicKeyHex === "string" &&80    /^[0-9a-f]{64}$/.test(r.longTermPublicKeyHex as string) &&81    typeof r.signingPublicKeyHex === "string" &&82    /^[0-9a-f]{64}$/.test(r.signingPublicKeyHex as string) &&83    typeof r.receivedAt === "number" &&84    typeof r.vouchedByDeviceId === "string"85  );86}