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
↓ peach-browser-4ec3741c.tar.gzatlas at this save
files changed2 files
src/background/messages.ts+126 −0src/background/vault.ts+8 −18src/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 {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[] {