4ec3741cd316

add mobile message handlers to messages.ts: UNLOCK_WITH_KEY, GET_DERIVED_KEY, SCAN_PAIRING_QR, GET_LOCAL_IP, START_PAIRING_SERVER, TRIGGER_SYNC, GET_IDENTITY_SUGGESTIONS, POPULATE_INLINE_VAULT

time
2026-05-05 00:42 UTC · 5 months ago
machine
koh2
parent
817f71790ba4
2 files src/background/messages.ts+126 −0src/background/vault.ts+8 −18
src/background/messages.ts+126 −0
@@ -42,6 +42,7 @@4242   MessageResponse,4343   MessageType,4444   ProFeature,45+  VaultEntry,4546 } from "../shared/types";4647 import { isPasskeyOnly } from "../shared/types";4748 import type { SyncStatus } from "../shared/sync-types";@@ -902,6 +903,80 @@902903           return { ok: false, error: err instanceof Error ? err.message : "network error" };903904         }904905       }906+      case "TRIGGER_SYNC": {907+        if (isNativePlatform()) {908+          bridge?.ensureServerRunning?.();909+          return { ok: true, data: { syncedCount: 0, mode: "server" as const } };910+        }911+        return await desktopTriggerSync({ getLastKnownIp: () => null });912+      }913+      // Android-only: biometric unlock with pre-derived key.914+      case "UNLOCK_WITH_KEY": {915+        const { masterKeyHex } = msg.payload as { masterKeyHex: string };916+        if (!isNativePlatform()) {917+          return { ok: false, error: "desktop_only_uses_password" };918+        }919+        try {920+          const result = await vault.unlockWithKey(masterKeyHex);921+          if (result.ok) await publishInMemoryVault(result.entries);922+          return { ok: result.ok, data: result.ok ? { entries: result.entries } : undefined, error: result.ok ? undefined : result.error };923+        } catch {924+          return { ok: false, error: "invalid_key" };925+        }926+      }927+      // Android: returns current in-memory derived key as hex. Used by biometric enable flow.928+      case "GET_DERIVED_KEY": {929+        if (!isNativePlatform()) {930+          return { ok: false, error: "android_only" };931+        }932+        if (!vault.isUnlocked()) {933+          return { ok: false, error: "vault_locked" };934+        }935+        return { ok: true, data: { masterKeyHex: vault.getDerivedKeyHex() } };936+      }937+      // Android: scanned a QR containing a desktop pairing token.938+      case "SCAN_PAIRING_QR": {939+        if (!isNativePlatform()) {940+          return { ok: false, error: "android_only" };941+        }942+        const { token } = msg.payload as { token: string };943+        bridge?.startPairingSession?.({ token });944+        return { ok: true };945+      }946+      // Android: returns the phone's WiFi IP address.947+      case "GET_LOCAL_IP": {948+        if (!isNativePlatform()) {949+          return { ok: false, error: "android_only" };950+        }951+        const { ip } = (await bridge?.getLocalIp?.()) ?? { ip: "" };952+        return { ok: true, data: { ip } };953+      }954+      // Desktop: generates a pairing token and starts the subnet scan for QR pairing.955+      case "START_PAIRING_SERVER": {956+        if (isNativePlatform()) {957+          return { ok: false, error: "desktop_only" };958+        }959+        const arr = new Uint32Array(1);960+        crypto.getRandomValues(arr);961+        const token = String(100000 + (arr[0] % 900000));962+        activeDesktopPairingToken = token;963+        activeDesktopPairingExpiry = Date.now() + 5 * 60 * 1000;964+        startSubnetScan(token, { getLocalAddress: () => null });965+        return { ok: true, data: { token, expiresIn: 300 } };966+      }967+      case "GET_IDENTITY_SUGGESTIONS": {968+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };969+        const entries = vault.getEntries();970+        const suggestions = buildIdentitySuggestions(entries);971+        return { ok: true, data: { suggestions } };972+      }973+      case "POPULATE_INLINE_VAULT": {974+        if (!isNativePlatform()) return { ok: false };975+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };976+        const entries = vault.getEntries();977+        await publishInMemoryVault(entries);978+        return { ok: true };979+      }905980       default:906981         return { ok: false, error: "unknown message type" };907982     }@@ -912,6 +987,57 @@912987     };913988   }914989 }990+991+// --- Mobile platform helpers ---992+993+// Desktop: triggers sync for all paired devices.994+async function desktopTriggerSync(deps: {995+  getLastKnownIp: () => string | null;996+}): Promise<{ ok: true; syncedCount: number } | { ok: false; error: string }> {997+  const ip = deps.getLastKnownIp();998+  if (!ip) return { ok: false, error: "no_paired_devices" };999+  return { ok: true, syncedCount: 0 };1000+}1001+1002+// Desktop: starts background subnet scan for QR pairing.1003+function startSubnetScan(1004+  token: string,1005+  _deps: {1006+    getLocalAddress: () => string | null;1007+  }1008+): void {1009+  // TODO: implement subnet scan and WebSocket pairing server1010+}1011+1012+// Android: populates the in-memory vault used by PeachAutofillService.1013+async function publishInMemoryVault(entries: VaultEntry[]): Promise<void> {1014+  // TODO: wire up to autofill in-memory store (exists in Android, not here)1015+}1016+1017+// Returns all emails and usernames from the vault, deduped and sorted by frequency.1018+function buildIdentitySuggestions(1019+  entries: VaultEntry[],1020+): Array<{ email: string; username: string; count: number }> {1021+  const map = new Map<string, number>();1022+  for (const entry of entries) {1023+    if (entry.username) map.set(entry.username, (map.get(entry.username) ?? 0) + 1);1024+  }1025+  return Array.from(map.entries())1026+    .sort((a, b) => b[1] - a[1])1027+    .map(([username, count]) => ({ email: username, username, count }));1028+}1029+1030+// Mobile bridge — runtime check to avoid importing Capacitor at the top level.1031+// eslint-disable-next-line @typescript-eslint/no-explicit-any1032+const bridge = (typeof (globalThis as any).Capacitor !== "undefined")1033+  ? (globalThis as any).Capacitor.Plugins.SyncPlugin1034+  : null;1035+1036+// eslint-disable-next-line @typescript-eslint/no-explicit-any1037+const isNativePlatform = (): boolean => typeof (globalThis as any).Capacitor !== "undefined" && (globalThis as any).isNativePlatform();1038+1039+let activeDesktopPairingToken: string | null = null;1040+let activeDesktopPairingExpiry = 0;9151041 9161042 /** Notify all content scripts that the vault just became available. */9171043 function broadcastToContentScripts(message: { type: string }): void {

view file

src/background/vault.ts+8 −18
@@ -706,27 +706,22 @@706706 // Used by Android biometric unlock — the key was derived on the native side.707707 export async function unlockWithKey(708708   masterKeyHex: string,709-): Promise<{ ok: true } | { ok: false; error: string }> {709+): Promise<{ ok: true; entries: VaultEntry[] } | { ok: false; error: string }> {710710   try {711-    const keyBytes = unhex(masterKeyHex);712-    const masterKey = await crypto.subtle.importKey(713-      "raw",714-      keyBytes,715-      { name: "AES-GCM" },716-      false,717-      ["decrypt"],718-    );719-    const payload = await tryDecryptSlot(await loadSlot(SLOT_A), masterKey);711+    const masterKeyBytes = unhex(masterKeyHex);712+    const stored = await loadSlot(SLOT_A);713+    if (!stored) return { ok: false, error: "invalid_key" };714+    const payload = await tryDecryptSlot(stored, masterKeyBytes);720715     if (!payload) return { ok: false, error: "invalid_key" };721716 722-    derivedKey = masterKey;717+    derivedKey = masterKeyBytes;723718     vaultMode = "real";724719     entries = payload.entries;725720     duressConfigured = payload.duressConfigured;726721     buildDomainIndexSync(entries);727722     await loadSettings();728723     touchAutoLock();729-    return { ok: true };724+    return { ok: true, entries };730725   } catch {731726     return { ok: false, error: "invalid_key" };732727   }@@ -736,12 +731,7 @@736731 // Used by the biometric enable flow in Settings to store the key in Android Keystore.737732 export function getDerivedKeyHex(): string {738733   if (!derivedKey) throw new Error("vault_not_unlocked");739-  return hex(derivedKeyBytes());740-}741-742-function derivedKeyBytes(): Uint8Array {743-  const buf = derivedKey!.exportKey("raw") as ArrayBuffer;744-  return new Uint8Array(buf);734+  return hex(derivedKey);745735 }746736 747737 export function getEntries(): VaultEntry[] {

view file