pending-vouched-devices.ts

3.0 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.1415export interface PendingVouchedDevice {16  deviceId: string;17  deviceName: string;18  deviceType: string;19  longTermPublicKeyHex: string;20  signingPublicKeyHex: string;21  wsPort?: number;22  /** pairedAt claimed by the vouching manifest — carried through on confirm. */23  pairedAt: number;24  receivedAt: number;25  /** deviceId of the extension whose signed manifest vouched for this device. */26  vouchedByDeviceId: string;27}2829const PENDING_KEY = "peach_pending_vouched_devices_v1";30const MAX_PENDING = 50;3132export async function listPendingVouchedDevices(): Promise<PendingVouchedDevice[]> {33  try {34    const result = await chrome.storage.local.get(PENDING_KEY);35    const value = (result as Record<string, unknown>)[PENDING_KEY];36    if (!Array.isArray(value)) return [];37    return value.filter(isPendingVouchedDevice);38  } catch {39    return [];40  }41}4243/** Add or refresh a pending device (keyed by deviceId). Bounded queue. */44export async function addPendingVouchedDevice(45  device: PendingVouchedDevice,46): Promise<void> {47  const pending = await listPendingVouchedDevices();48  const next = pending.filter((d) => d.deviceId !== device.deviceId);49  next.push(device);50  await chrome.storage.local.set({ [PENDING_KEY]: next.slice(-MAX_PENDING) });51}5253export async function getPendingVouchedDevice(54  deviceId: string,55): Promise<PendingVouchedDevice | null> {56  const pending = await listPendingVouchedDevices();57  return pending.find((d) => d.deviceId === deviceId) ?? null;58}5960export async function removePendingVouchedDevice(deviceId: string): Promise<void> {61  const pending = await listPendingVouchedDevices();62  await chrome.storage.local.set({63    [PENDING_KEY]: pending.filter((d) => d.deviceId !== deviceId),64  });65}6667function isPendingVouchedDevice(x: unknown): x is PendingVouchedDevice {68  if (typeof x !== "object" || x === null) return false;69  const r = x as Record<string, unknown>;70  return (71    typeof r.deviceId === "string" && !!r.deviceId &&72    typeof r.deviceName === "string" &&73    typeof r.deviceType === "string" &&74    typeof r.longTermPublicKeyHex === "string" &&75    /^[0-9a-f]{64}$/.test(r.longTermPublicKeyHex as string) &&76    typeof r.signingPublicKeyHex === "string" &&77    /^[0-9a-f]{64}$/.test(r.signingPublicKeyHex as string) &&78    typeof r.receivedAt === "number" &&79    typeof r.vouchedByDeviceId === "string"80  );81}