a1d75b982320
add biometric unlock path to LockView: hasMasterKey + isAvailable check on mount, auto-prompt 400ms after, BiometricPlugin.authenticate -> KeystorePlugin.retrieveMasterKey -> UNLOCK_WITH_KEY; biometric button rendered when available; biometric_key_invalidated falls back to password and clears biometricsEnabled
- time
- 2026-05-07 02:21 UTC · 5 months ago
- machine
- koh2
- parent
- 3872f604f17d
↓ peach-browser-a1d75b98.tar.gzatlas at this save
files changed1 file
src/popup/views/Lock.tsx+93 −0src/popup/views/Lock.tsx+93 −0
@@ -10,6 +10,7 @@1010 import { isPeachScript, decodePeachScript, normalisePeachScript } from "../../crypto/peachScript";1111 import { parsePayload } from "../../crypto/peachPayload";1212 import { parseImportSource } from "../../shared/import";13+import { isNativePlatform, nativePlugin } from "../../shared/native";1314 1415 const peachMarkImg = new Image();1516 peachMarkImg.src = `data:image/svg+xml,${encodeURIComponent(@@ -64,6 +65,8 @@6465 const MIN_ZXCVBN_SCORE = 2;6566 6667 const [strengthScore, setStrengthScore] = createSignal<number | null>(null);68+ const [biometricAvailable, setBiometricAvailable] = createSignal(false);69+ const [biometricBusy, setBiometricBusy] = createSignal(false);6770 6871 const canCreate = createMemo(6972 () =>@@ -127,6 +130,75 @@127130 setLoading(false);128131 }129132 133+ async function checkBiometricAvailable(): Promise<void> {134+ if (!props.hasVault || !isNativePlatform()) return;135+ const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin");136+ const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin");137+ if (!keystore || !biometric) return;138+ try {139+ const stored = await keystore.hasMasterKey();140+ if (!stored.exists) return;141+ const avail = await biometric.isAvailable();142+ if (!avail.available) return;143+ setBiometricAvailable(true);144+ window.setTimeout(() => { void handleBiometricUnlock(); }, 400);145+ } catch { /* missing plugins or runtime error — silently skip */ }146+ }147+148+ async function handleBiometricUnlock(): Promise<void> {149+ if (biometricBusy()) return;150+ const biometric = nativePlugin<{ authenticate: (opts: Record<string, string>) => Promise<void> }>("BiometricPlugin");151+ const keystore = nativePlugin<{ retrieveMasterKey: () => Promise<{ masterKeyHex: string }> }>("KeystorePlugin");152+ if (!biometric || !keystore) return;153+ setBiometricBusy(true);154+ setError("");155+ try {156+ await biometric.authenticate({157+ title: "Unlock Peach",158+ subtitle: "Use biometrics to access your vault",159+ negativeButtonText: "Use Password",160+ });161+ const { masterKeyHex } = await keystore.retrieveMasterKey();162+ const res = (await sendMessage({163+ type: "UNLOCK_WITH_KEY",164+ payload: { masterKeyHex },165+ })) as { ok: boolean; error?: string };166+ if (res.ok) {167+ await props.onUnlock();168+ return;169+ }170+ if (res.error === "biometric_key_invalidated") {171+ await sendMessage({172+ type: "UPDATE_SETTINGS",173+ payload: { biometricsEnabled: false },174+ });175+ setBiometricAvailable(false);176+ setError("biometric key expired — re-enable in settings");177+ } else {178+ setError("biometric unlock failed");179+ }180+ } catch (e) {181+ const reason = e instanceof Error ? e.message : String(e);182+ if (reason.includes("user_cancelled")) return;183+ if (reason.includes("biometric_key_invalidated")) {184+ await sendMessage({185+ type: "UPDATE_SETTINGS",186+ payload: { biometricsEnabled: false },187+ });188+ setBiometricAvailable(false);189+ setError("biometric key expired — re-enable in settings");190+ return;191+ }192+ if (reason.includes("biometric_lockout")) {193+ setError("biometric locked out — use your password");194+ }195+ } finally {196+ setBiometricBusy(false);197+ }198+ }199+200+ onMount(() => { void checkBiometricAvailable(); });201+130202 async function handleCreate() {131203 if (!password().trim()) return;132204 if (password().length < MIN_LENGTH) {@@ -261,6 +333,27 @@261333 >262334 {loading() ? "unlocking..." : "unlock"}263335 </button>336+337+ <Show when={biometricAvailable()}>338+ <button339+ class="peach-biometric-btn"340+ type="button"341+ onClick={() => void handleBiometricUnlock()}342+ disabled={biometricBusy() || loading()}343+ aria-label="Unlock with biometrics"344+ >345+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">346+ <path d="M12 11c-1.7 0-3 1.3-3 3v3"/>347+ <path d="M15 17v-3"/>348+ <path d="M9 4.5C9.7 4.2 10.8 4 12 4s2.3.2 3 .5"/>349+ <path d="M5 9c.7-1.6 2-2.9 3.6-3.6"/>350+ <path d="M19 9c-.7-1.6-2-2.9-3.6-3.6"/>351+ <path d="M5 15c0-1.7.3-3.3.8-4.7"/>352+ <path d="M19 15c0-1.7-.3-3.3-.8-4.7"/>353+ </svg>354+ {biometricBusy() ? "verifying…" : "use biometrics"}355+ </button>356+ </Show>264357 265358 <button266359 class="peach-lock-recover-link"