1import { isProUser } from "./license";2import * as vault from "./vault";3import {4 base64urlDecode,5 base64urlEncode,6 buildAuthenticatorData,7 buildClientDataJSON,8 createAttestationObject,9 generateCredentialId,10 generatePasskeyKeypair,11 pickPasskeyAlgorithm,12 signPasskeyAssertion,13} from "./passkey-crypto";14import { detectBrowser as detectSharedBrowser, firefoxVersion } from "../shared/browser";15import { FIREFOX_USE_CONTENT_SCRIPT_PROXY } from "../shared/passkey-flags";16import type {17 PasskeyData,18 PasskeyProviderState,19 PendingPasskeyRequest,20 VaultEntry,21} from "../shared/types";2223declare const __PEACH_E2E_PASSKEYS__: boolean;2425const PENDING_PREFIX = "passkey_pending_";26const APPROVAL_TIMEOUT_MS = 60_000;2728let listenersRegistered = false;29let attached = false;30let conflict: string | undefined;31const PASSKEY_CREATE_E2E_BYPASS = __PEACH_E2E_PASSKEYS__ === true;3233const approvals = new Map<34 number,35 { resolve: (approved: boolean) => void; timeoutId: ReturnType<typeof setTimeout> }36>();3738function isPasskeySupported(): boolean {39 if (typeof chrome.webAuthenticationProxy?.attach === "function") return true;40 41 42 43 44 if (detectSharedBrowser() === "firefox") {45 const ver = firefoxVersion();46 if (Number.isFinite(ver) && ver >= 150) return true;47 }48 return false;49}5051export const detectBrowser = detectSharedBrowser;5253function getBrowserLabel(): string {54 const ua = navigator.userAgent;55 if (/Firefox\/(\d+)/.test(ua)) {56 const version = ua.match(/Firefox\/(\d+)/)?.[1];57 return version ? `Firefox ${version}` : "Firefox";58 }59 const version = ua.match(/Chrome\/(\d+)/)?.[1];60 return version ? `Chrome ${version}` : "Chromium";61}6263function pendingStorageKey(requestId: number): string {64 return `${PENDING_PREFIX}${requestId}`;65}6667function originFromRpId(rpId: string): string {68 return /^(localhost|127\.0\.0\.1|\[::1\])$/i.test(rpId)69 ? `http://${rpId}`70 : `https://${rpId}`;71}7273async function setBadge(text: string): Promise<void> {74 if (!chrome.action?.setBadgeText) return;75 await chrome.action.setBadgeText({ text });76 if (text) {77 await chrome.action.setBadgeBackgroundColor({ color: "#D96830" });78 }79}8081async function clearPendingRequest(requestId: number): Promise<void> {82 if (chrome.storage.session) {83 await chrome.storage.session.remove(pendingStorageKey(requestId));84 }85 await setBadge("");86}8788async function openApprovalPopup(): Promise<void> {89 try {90 await chrome.action.openPopup();91 } catch {92 await setBadge("PK");93 }94}9596async function rejectCreate(97 requestId: number,98 name: string,99 message = ""100): Promise<void> {101 await chrome.webAuthenticationProxy.completeCreateRequest({102 requestId,103 error: { name, message },104 });105}106107async function rejectGet(108 requestId: number,109 name: string,110 message = ""111): Promise<void> {112 await chrome.webAuthenticationProxy.completeGetRequest({113 requestId,114 error: { name, message },115 });116}117118export async function getPendingPasskeyRequest(): Promise<PendingPasskeyRequest | null> {119 if (!chrome.storage.session) return null; 120 const stored = (await chrome.storage.session.get(null)) as Record<string, unknown>;121 const key = Object.keys(stored).find((item) => item.startsWith(PENDING_PREFIX));122 if (!key) return null;123 const pending = stored[key];124 return pending && typeof pending === "object"125 ? (pending as PendingPasskeyRequest)126 : null;127}128129export async function resolvePendingPasskeyRequest(130 requestId: number,131 approved: boolean132): Promise<void> {133 const pending = approvals.get(requestId);134 if (!pending) {135 136 137 138 139 await clearPendingRequest(requestId);140 if (chrome.storage.session) {141 await chrome.storage.session.remove(FIREFOX_PENDING_KEY);142 }143 return;144 }145146 clearTimeout(pending.timeoutId);147 approvals.delete(requestId);148 await clearPendingRequest(requestId);149 pending.resolve(approved);150}151152async function promptUserApproval(request: PendingPasskeyRequest): Promise<boolean> {153 if (chrome.storage.session) {154 await chrome.storage.session.set({ [pendingStorageKey(request.requestId)]: request });155 }156 await openApprovalPopup();157158 return new Promise<boolean>((resolve) => {159 const timeoutId = setTimeout(async () => {160 approvals.delete(request.requestId);161 await clearPendingRequest(request.requestId);162 resolve(false);163 }, APPROVAL_TIMEOUT_MS);164165 approvals.set(request.requestId, { resolve, timeoutId });166 });167}168169export async function getPasskeyProviderState(): Promise<PasskeyProviderState> {170 const settings = await vault.ensureSettingsLoaded();171 const supported = isPasskeySupported();172 const enabled = settings.passkeyProviderEnabled === true;173 return {174 supported,175 enabled,176 attached: supported && attached,177 browserLabel: getBrowserLabel(),178 statusLabel: !supported179 ? "Interception unavailable"180 : attached181 ? "Active"182 : conflict183 ? "Another provider is active"184 : enabled185 ? "Inactive"186 : "Disabled",187 conflict,188 };189}190191async function attachPasskeyProxy(): Promise<void> {192 if (!isPasskeySupported()) {193 attached = false;194 conflict = "This browser does not expose chrome.webAuthenticationProxy.";195 return;196 }197198 199 200 201 if (detectSharedBrowser() === "firefox") {202 attached = true;203 conflict = undefined;204 return;205 }206207 if (attached) return;208209 try {210 await chrome.webAuthenticationProxy.attach();211 attached = true;212 conflict = undefined;213 } catch (error) {214 attached = false;215 conflict = error instanceof Error ? error.message : "Another provider is active.";216 }217}218219async function detachPasskeyProxy(): Promise<void> {220 if (!isPasskeySupported()) {221 attached = false;222 return;223 }224225 if (detectSharedBrowser() === "firefox") {226 attached = false;227 conflict = undefined;228 return;229 }230231 try {232 await chrome.webAuthenticationProxy.detach();233 } catch {234 235 }236237 attached = false;238 conflict = undefined;239}240241export async function setPasskeyProviderEnabled(enabled: boolean): Promise<PasskeyProviderState> {242 await vault.updateSettings({ passkeyProviderEnabled: enabled });243 if (enabled) {244 await attachPasskeyProxy();245 } else {246 await detachPasskeyProxy();247 }248 return getPasskeyProviderState();249}250251async function refreshPasskeyProxy(): Promise<void> {252 const settings = await vault.ensureSettingsLoaded();253 if (settings.passkeyProviderEnabled === true) {254 await attachPasskeyProxy();255 return;256 }257 await detachPasskeyProxy();258}259260261262263264265266267function findAttachTarget(rpId: string): VaultEntry | null {268 const all = vault.getEntries();269 const passkeyMatch = all.find(270 (entry) => entry.passkey?.rpId === rpId && !entry.deletedAt271 );272 if (passkeyMatch) return passkeyMatch;273 const loginMatches = vault.queryDomain(rpId).filter((entry) => entry.type === "login" && !entry.passkey);274 if (loginMatches.length === 0) return null;275 276 return loginMatches.reduce((best, e) =>277 (e.modifiedAt ?? 0) > (best.modifiedAt ?? 0) ? e : best278 );279}280281async function handleCreateRequest(282 request: chrome.webAuthenticationProxy.CreateRequest283): Promise<void> {284 const options = JSON.parse(request.requestDetailsJson) as PublicKeyCredentialCreationOptionsJSON;285 const rpId = options.rp.id;286 const rpName = options.rp.name ?? options.rp.id;287 const userName = options.user.name;288 const userDisplayName = options.user.displayName ?? options.user.name;289290 if (!rpId) {291 await rejectCreate(request.requestId, "NotSupportedError", "Missing RP ID");292 return;293 }294295 if (!PASSKEY_CREATE_E2E_BYPASS && !(await isProUser())) {296 await rejectCreate(297 request.requestId,298 "NotAllowedError",299 "Passkey creation requires Peach Pro."300 );301 302 broadcastPasskeyProGate();303 return;304 }305306 if (!vault.isUnlocked()) {307 await openApprovalPopup();308 await rejectCreate(request.requestId, "NotAllowedError", "Unlock Peach first.");309 return;310 }311312 const approved = await promptUserApproval({313 requestId: request.requestId,314 type: "create",315 rpId,316 rpName,317 userName,318 userDisplayName,319 });320 if (!approved) {321 await rejectCreate(request.requestId, "NotAllowedError", "User denied request.");322 return;323 }324325 const algorithm = pickPasskeyAlgorithm(options.pubKeyCredParams ?? []);326 if (!algorithm) {327 await rejectCreate(request.requestId, "NotSupportedError", "No supported algorithm offered.");328 return;329 }330331 const credentialIdBytes = crypto.getRandomValues(new Uint8Array(32));332 const { publicKeyCose, publicKeySpki, privateKeyJwk } = await generatePasskeyKeypair(333 algorithm334 );335 const authData = await buildAuthenticatorData({336 rpId,337 flags: 0x4d,338 signCount: 0,339 credentialId: credentialIdBytes,340 publicKeyCose,341 });342 const attestationObject = createAttestationObject(authData);343 const clientDataJSON = buildClientDataJSON("webauthn.create", options.challenge, originFromRpId(rpId));344345 const passkey: PasskeyData = {346 rpId,347 rpName,348 credentialId: base64urlEncode(credentialIdBytes),349 publicKeyCose: base64urlEncode(publicKeyCose),350 privateKeyJwk,351 userHandle: options.user.id,352 userName,353 userDisplayName,354 algorithm,355 signCount: 0,356 aaguid: base64urlEncode(new Uint8Array(16)),357 backupEligible: false,358 backupState: false,359 browser: "chrome",360 transports: ["internal"],361 createdAt: Date.now(),362 };363364 365 366 const target = findAttachTarget(rpId);367 if (target) {368 await vault.updateEntry(target.id, { passkey });369 } else {370 await vault.addEntry({371 type: "login",372 title: rpName,373 url: `https://${rpId}`,374 username: userName,375 passkey,376 });377 }378379 await chrome.webAuthenticationProxy.completeCreateRequest({380 requestId: request.requestId,381 responseJson: JSON.stringify({382 id: passkey.credentialId,383 rawId: passkey.credentialId,384 type: "public-key",385 response: {386 clientDataJSON: base64urlEncode(clientDataJSON),387 authenticatorData: base64urlEncode(authData),388 attestationObject: base64urlEncode(attestationObject),389 transports: ["internal"],390 publicKey: base64urlEncode(publicKeySpki),391 publicKeyAlgorithm: algorithm,392 },393 authenticatorAttachment: "platform",394 clientExtensionResults: {},395 }),396 });397}398399function broadcastPasskeyProGate(): void {400 chrome.tabs.query({}, (tabs) => {401 for (const tab of tabs) {402 if (tab.id !== undefined) {403 chrome.tabs.sendMessage(tab.id, { type: "PASSKEY_PRO_GATE" }).catch(() => {});404 }405 }406 });407}408409async function handleGetRequest(410 request: chrome.webAuthenticationProxy.GetRequest411): Promise<void> {412 const options = JSON.parse(request.requestDetailsJson) as PublicKeyCredentialRequestOptionsJSON;413 if (!vault.isUnlocked()) {414 await openApprovalPopup();415 await rejectGet(request.requestId, "NotAllowedError", "Unlock Peach first.");416 return;417 }418419 if (!(await isProUser())) {420 await rejectGet(request.requestId, "NotAllowedError", "Passkey use requires Peach Pro.");421 broadcastPasskeyProGate();422 return;423 }424425 const passkeyBearing = vault.getEntries().filter((entry): entry is VaultEntry & { passkey: PasskeyData } =>426 entry.passkey !== undefined && !entry.deletedAt427 );428 const allowedCredentialIds = (options.allowCredentials ?? []).map((descriptor) => descriptor.id);429430 const match = passkeyBearing.find((entry) => {431 if (allowedCredentialIds.length > 0) {432 return allowedCredentialIds.includes(entry.passkey.credentialId);433 }434 if (!options.rpId) return false;435 return entry.passkey.rpId === options.rpId;436 });437438 if (!match) {439 await rejectGet(request.requestId, "NotAllowedError", "No matching passkey found.");440 return;441 }442443 const credential = match.passkey;444 445 446 447 448 if (!credential.privateKeyJwk) {449 await rejectGet(450 request.requestId,451 "NotAllowedError",452 "This passkey was created on Firefox; sign in via the platform authenticator."453 );454 return;455 }456457 const approved = await promptUserApproval({458 requestId: request.requestId,459 type: "get",460 rpId: credential.rpId,461 rpName: credential.rpName,462 userName: credential.userName,463 userDisplayName: credential.userDisplayName,464 });465 if (!approved) {466 await rejectGet(request.requestId, "NotAllowedError", "User denied request.");467 return;468 }469470 const nextSignCount = credential.signCount + 1;471 const flags = 0x05 | (credential.backupEligible ? 0x08 : 0) | (credential.backupState ? 0x10 : 0);472 const authData = await buildAuthenticatorData({473 rpId: credential.rpId,474 flags,475 signCount: nextSignCount,476 });477 const clientDataJSON = buildClientDataJSON("webauthn.get", options.challenge, originFromRpId(credential.rpId));478 const clientDataHash = new Uint8Array(479 await crypto.subtle.digest("SHA-256", new Uint8Array(clientDataJSON))480 );481 const signature = await signPasskeyAssertion(482 credential.privateKeyJwk,483 credential.algorithm,484 authData,485 clientDataHash486 );487488 await vault.updateEntry(match.id, {489 passkey: {490 ...credential,491 signCount: nextSignCount,492 lastUsedAt: Date.now(),493 },494 });495496 const responseJson = JSON.stringify({497 id: credential.credentialId,498 rawId: credential.credentialId,499 type: "public-key",500 response: {501 clientDataJSON: base64urlEncode(clientDataJSON),502 authenticatorData: base64urlEncode(authData),503 signature: base64urlEncode(signature),504 userHandle: credential.userHandle,505 },506 authenticatorAttachment: "platform",507 clientExtensionResults: {},508 });509510 await chrome.webAuthenticationProxy.completeGetRequest({511 requestId: request.requestId,512 responseJson,513 });514}515516517518519520521522523524525const FIREFOX_PENDING_KEY = "passkey_pending_firefox";526const FIREFOX_DECLINE_PREFIX = "peach_passkey_offer_declined:";527const FIREFOX_DECLINE_TTL_MS = 30 * 24 * 60 * 60 * 1000; 528529export async function initiateFirefoxPasskeyCreate(args: {530 rpId: string;531 rpName: string;532 userName: string;533 userDisplayName?: string;534}): Promise<{ ok: boolean; error?: string }> {535 if (!(await isProUser())) {536 broadcastPasskeyProGate();537 return { ok: false, error: "passkey creation requires Peach Pro" };538 }539 if (!vault.isUnlocked()) {540 await openApprovalPopup();541 return { ok: false, error: "vault locked" };542 }543 if (!chrome.storage.session) {544 return { ok: false, error: "session storage unavailable" };545 }546547 const requestId = Date.now();548 const challenge = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));549 const userHandle = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));550551 const pending: PendingPasskeyRequest = {552 requestId,553 type: "create",554 rpId: args.rpId,555 rpName: args.rpName,556 userName: args.userName,557 userDisplayName: args.userDisplayName,558 browser: "firefox",559 challenge,560 userHandle,561 };562563 await chrome.storage.session.set({ [FIREFOX_PENDING_KEY]: pending });564 await openApprovalPopup();565 return { ok: true };566}567568export async function completeFirefoxPasskeyCreate(payload: {569 requestId: number;570 rpId: string;571 rpName: string;572 userName: string;573 userDisplayName?: string;574 userHandle: string;575 credentialId: string;576 publicKeyCose: string;577 algorithm: -7 | -8;578 aaguid?: string;579 transports?: string[];580 backupEligible?: boolean;581 backupState?: boolean;582}): Promise<{ ok: boolean; error?: string }> {583 if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };584585 const passkey: PasskeyData = {586 rpId: payload.rpId,587 rpName: payload.rpName,588 credentialId: payload.credentialId,589 publicKeyCose: payload.publicKeyCose,590 userHandle: payload.userHandle,591 userName: payload.userName,592 userDisplayName: payload.userDisplayName ?? payload.userName,593 algorithm: payload.algorithm,594 signCount: 0,595 aaguid: payload.aaguid ?? base64urlEncode(new Uint8Array(16)),596 backupEligible: payload.backupEligible ?? false,597 backupState: payload.backupState ?? false,598 browser: "firefox",599 transports: payload.transports ?? ["internal"],600 createdAt: Date.now(),601 };602603 604 605 606 const target = findAttachTarget(payload.rpId);607 if (target) {608 await vault.updateEntry(target.id, { passkey });609 } else {610 await vault.addEntry({611 type: "login",612 title: payload.rpName || payload.rpId,613 url: `https://${payload.rpId}`,614 username: payload.userName,615 passkey,616 });617 }618619 if (chrome.storage.session) {620 await chrome.storage.session.remove(FIREFOX_PENDING_KEY);621 }622 return { ok: true };623}624625export async function cancelFirefoxPasskeyCreate(): Promise<void> {626 if (chrome.storage.session) {627 await chrome.storage.session.remove(FIREFOX_PENDING_KEY);628 }629}630631function declineKey(domain: string): string {632 return `${FIREFOX_DECLINE_PREFIX}${domain}`;633}634635export async function recordPasskeyOfferDecline(domain: string): Promise<void> {636 if (!domain) return;637 await chrome.storage.local.set({ [declineKey(domain)]: Date.now() });638}639640export async function isPasskeyOfferDeclined(domain: string): Promise<boolean> {641 if (!domain) return false;642 const key = declineKey(domain);643 const stored = (await chrome.storage.local.get(key)) as Record<string, number | undefined>;644 const at = stored[key];645 if (typeof at !== "number") return false;646 if (Date.now() - at > FIREFOX_DECLINE_TTL_MS) {647 await chrome.storage.local.remove(key);648 return false;649 }650 return true;651}652653654655656657658659660661662663664665666interface FirefoxAllowCredential {667 id: string;668 type?: string;669}670671interface FirefoxPubKeyCredParam {672 type?: string;673 alg: number;674}675676export interface FirefoxPasskeyHasCredentialsPayload {677 rpId: string;678 allowCredentials: FirefoxAllowCredential[];679}680681export interface FirefoxPasskeyCreatePayload {682 rpId: string;683 rpName?: string;684 userId: string;685 userName: string;686 userDisplayName?: string;687 challenge: string;688 clientDataJSON: number[];689 origin: string;690 pubKeyCredParams?: FirefoxPubKeyCredParam[];691 authenticatorSelection?: Record<string, unknown>;692 timeout?: number;693 attestation?: string;694}695696export interface FirefoxPasskeyGetPayload {697 rpId: string;698 challenge: string;699 clientDataJSON: number[];700 origin: string;701 allowCredentials: FirefoxAllowCredential[];702 userVerification?: string;703}704705interface FirefoxCreateCredentialResponse {706 type: "create";707 id: string;708 clientDataJSON: number[];709 attestationObject: string;710 authenticatorData: string;711 publicKey: string;712 algorithm: -7 | -8;713}714715interface FirefoxGetCredentialResponse {716 type: "get";717 id: string;718 clientDataJSON: number[];719 authenticatorData: string;720 signature: string;721 userHandle: string;722}723724type FirefoxPasskeyResult<T> =725 | { ok: true; credential: T }726 | { ok: false; error: "pro_required" | "user_cancelled" | "no_credentials" | "vault_locked" | "not_applicable" | "internal_error"; message?: string };727728function listPasskeyEntries(): Array<VaultEntry & { passkey: PasskeyData }> {729 return vault.getEntries().filter(730 (entry): entry is VaultEntry & { passkey: PasskeyData } =>731 entry.passkey !== undefined && !entry.deletedAt732 );733}734735export async function handlePasskeyHasCredentials(736 payload: FirefoxPasskeyHasCredentialsPayload737): Promise<{ ok: true; hasCredentials: boolean } | { ok: false; error: string }> {738 if (!FIREFOX_USE_CONTENT_SCRIPT_PROXY) {739 return { ok: false, error: "not_applicable" };740 }741 if (!vault.isUnlocked()) {742 743 744 745 return { ok: true, hasCredentials: false };746 }747 const passkeys = listPasskeyEntries().filter(748 (entry) => entry.passkey.rpId === payload.rpId749 );750 if (passkeys.length === 0) return { ok: true, hasCredentials: false };751752 const allow = payload.allowCredentials ?? [];753 if (allow.length === 0) return { ok: true, hasCredentials: true };754755 const hasMatch = passkeys.some((entry) =>756 allow.some((ac) => ac.id === entry.passkey.credentialId)757 );758 return { ok: true, hasCredentials: hasMatch };759}760761762763764765766767768async function requestFirefoxApproval(args: {769 type: "create" | "get";770 rpId: string;771 rpName?: string;772 userName: string;773 userDisplayName?: string;774}): Promise<boolean> {775 776 777 const requestId = Date.now() + Math.floor(Math.random() * 1000);778 return promptUserApproval({779 requestId,780 type: args.type,781 rpId: args.rpId,782 rpName: args.rpName ?? args.rpId,783 userName: args.userName,784 userDisplayName: args.userDisplayName,785 });786}787788export async function handleFirefoxPasskeyCreate(789 payload: FirefoxPasskeyCreatePayload790): Promise<FirefoxPasskeyResult<FirefoxCreateCredentialResponse>> {791 if (!FIREFOX_USE_CONTENT_SCRIPT_PROXY) {792 return { ok: false, error: "not_applicable" };793 }794 if (!(await isProUser())) {795 broadcastPasskeyProGate();796 return { ok: false, error: "pro_required" };797 }798 if (!vault.isUnlocked()) {799 await openApprovalPopup();800 return { ok: false, error: "vault_locked" };801 }802803 const algorithm = pickPasskeyAlgorithm(payload.pubKeyCredParams ?? []);804 if (!algorithm) {805 return { ok: false, error: "internal_error", message: "No supported algorithm offered" };806 }807808 const approved = await requestFirefoxApproval({809 type: "create",810 rpId: payload.rpId,811 rpName: payload.rpName,812 userName: payload.userName,813 userDisplayName: payload.userDisplayName,814 });815 if (!approved) return { ok: false, error: "user_cancelled" };816817 const { bytes: credentialIdBytes, base64url: credentialId } = generateCredentialId();818 const { publicKeyCose, publicKeySpki, privateKeyJwk } = await generatePasskeyKeypair(algorithm);819820 821 822 const authenticatorData = await buildAuthenticatorData({823 rpId: payload.rpId,824 flags: 0x45,825 signCount: 0,826 credentialId: credentialIdBytes,827 publicKeyCose,828 });829 const attestationObject = createAttestationObject(authenticatorData);830831 const passkey: PasskeyData = {832 rpId: payload.rpId,833 rpName: payload.rpName ?? payload.rpId,834 credentialId,835 publicKeyCose: base64urlEncode(publicKeyCose),836 privateKeyJwk,837 userHandle: payload.userId,838 userName: payload.userName,839 userDisplayName: payload.userDisplayName ?? payload.userName,840 algorithm,841 signCount: 0,842 aaguid: base64urlEncode(new Uint8Array(16)),843 backupEligible: false,844 backupState: false,845 browser: "firefox",846 transports: ["internal"],847 createdAt: Date.now(),848 };849850 const target = findAttachTarget(payload.rpId);851 if (target) {852 await vault.updateEntry(target.id, { passkey });853 } else {854 await vault.addEntry({855 type: "login",856 title: payload.rpName || payload.rpId,857 url: `https://${payload.rpId}`,858 username: payload.userName,859 passkey,860 });861 }862863 return {864 ok: true,865 credential: {866 type: "create",867 id: credentialId,868 869 870 871 clientDataJSON: payload.clientDataJSON,872 attestationObject: base64urlEncode(attestationObject),873 authenticatorData: base64urlEncode(authenticatorData),874 publicKey: base64urlEncode(publicKeySpki),875 algorithm,876 },877 };878}879880export async function handleFirefoxPasskeyGet(881 payload: FirefoxPasskeyGetPayload882): Promise<FirefoxPasskeyResult<FirefoxGetCredentialResponse>> {883 if (!FIREFOX_USE_CONTENT_SCRIPT_PROXY) {884 return { ok: false, error: "not_applicable" };885 }886 if (!(await isProUser())) {887 broadcastPasskeyProGate();888 return { ok: false, error: "pro_required" };889 }890 if (!vault.isUnlocked()) {891 await openApprovalPopup();892 return { ok: false, error: "vault_locked" };893 }894895 const allow = payload.allowCredentials ?? [];896 const candidates = listPasskeyEntries().filter((entry) => {897 if (entry.passkey.rpId !== payload.rpId) return false;898 if (allow.length === 0) return true;899 return allow.some((ac) => ac.id === entry.passkey.credentialId);900 });901902 if (candidates.length === 0) return { ok: false, error: "no_credentials" };903904 905 906 907 candidates.sort((a, b) => (b.passkey.lastUsedAt ?? 0) - (a.passkey.lastUsedAt ?? 0));908 const match = candidates[0]!;909 const credential = match.passkey;910911 if (!credential.privateKeyJwk) {912 913 914 915 return { ok: false, error: "no_credentials" };916 }917918 const approved = await requestFirefoxApproval({919 type: "get",920 rpId: credential.rpId,921 rpName: credential.rpName,922 userName: credential.userName,923 userDisplayName: credential.userDisplayName,924 });925 if (!approved) return { ok: false, error: "user_cancelled" };926927 const nextSignCount = credential.signCount + 1;928 929 930 const flags =931 0x05 |932 (credential.backupEligible ? 0x08 : 0) |933 (credential.backupState ? 0x10 : 0);934 const authenticatorData = await buildAuthenticatorData({935 rpId: credential.rpId,936 flags,937 signCount: nextSignCount,938 });939940 941 942 943 const clientDataBytes = new Uint8Array(payload.clientDataJSON);944 const clientDataHash = new Uint8Array(945 await crypto.subtle.digest("SHA-256", clientDataBytes)946 );947 const signature = await signPasskeyAssertion(948 credential.privateKeyJwk,949 credential.algorithm,950 authenticatorData,951 clientDataHash952 );953954 await vault.updateEntry(match.id, {955 passkey: {956 ...credential,957 signCount: nextSignCount,958 lastUsedAt: Date.now(),959 },960 });961962 return {963 ok: true,964 credential: {965 type: "get",966 id: credential.credentialId,967 clientDataJSON: payload.clientDataJSON,968 authenticatorData: base64urlEncode(authenticatorData),969 signature: base64urlEncode(signature),970 userHandle: credential.userHandle,971 },972 };973}974975export async function setupPasskeyProxy(): Promise<void> {976 if (!isPasskeySupported()) return;977978 979 980 if (detectSharedBrowser() === "firefox") {981 await refreshPasskeyProxy();982 return;983 }984985 if (!listenersRegistered) {986 chrome.webAuthenticationProxy.onIsUvpaaRequest.addListener((requestInfo) => {987 void chrome.webAuthenticationProxy.completeIsUvpaaRequest({988 requestId: requestInfo.requestId,989 isUvpaa: vault.isUnlocked(),990 });991 });992 chrome.webAuthenticationProxy.onCreateRequest.addListener((requestInfo) => {993 void handleCreateRequest(requestInfo).catch(async (error) => {994 console.error("Peach passkey create failed", error);995 await rejectCreate(996 requestInfo.requestId,997 "UnknownError",998 error instanceof Error ? error.message : "Passkey creation failed."999 );1000 });1001 });1002 chrome.webAuthenticationProxy.onGetRequest.addListener((requestInfo) => {1003 void handleGetRequest(requestInfo).catch(async (error) => {1004 console.error("Peach passkey get failed", error);1005 await rejectGet(1006 requestInfo.requestId,1007 "UnknownError",1008 error instanceof Error ? error.message : "Passkey authentication failed."1009 );1010 });1011 });1012 chrome.webAuthenticationProxy.onRequestCanceled.addListener((requestId) => {1013 void resolvePendingPasskeyRequest(requestId, false);1014 });1015 listenersRegistered = true;1016 }10171018 await refreshPasskeyProxy();1019}