123456789101112131415161718192021222324252627282930313233343536373839404142434445declare function exportFunction(46 func: (...args: any[]) => any,47 targetScope: any,48 options?: { defineAs?: string },49): (...args: any[]) => any;5051declare function cloneInto<T>(52 obj: T,53 targetScope: any,54 options?: { cloneFunctions?: boolean },55): T;5657export function installPeachPasskeyProxy(): void {58 const w = window as unknown as Record<string, unknown>;59 if (w.__peachPasskeyProxyInstalled) return;60 w.__peachPasskeyProxyInstalled = true;6162 63 64 65 66 const sendMessage = chrome.runtime.sendMessage;6768 69 70 71 if (typeof exportFunction !== "function" || typeof cloneInto !== "function") {72 return;73 }7475 const credentials = navigator.credentials;76 if (!credentials || typeof credentials.create !== "function" || typeof credentials.get !== "function") {77 return;78 }7980 81 const pageCredentials =82 (credentials as unknown as { wrappedJSObject?: CredentialsContainer }).wrappedJSObject ??83 credentials;8485 86 const originalCreate = credentials.create.bind(credentials);87 const originalGet = credentials.get.bind(credentials);8889 function pagePromise<T>(task: () => Promise<T>): Promise<T> {90 return new window.Promise<T>((resolve, reject) => {91 task().then(resolve, reject);92 });93 }9495 exportFunction(96 function patchedCreate(options?: CredentialCreationOptions): Promise<Credential | null> {97 return pagePromise(async () => {98 if (!options?.publicKey) return originalCreate(options);99100 try {101 const publicKey = options.publicKey;102 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));103 const clientDataJSON = encodeJsonBytes({104 type: "webauthn.create",105 challenge,106 origin: window.location.origin,107 crossOrigin: false,108 });109110 const userId = bufferToBase64url(toArrayBuffer(publicKey.user.id));111112 const response = await sendMessage({113 type: "PASSKEY_CREATE",114 payload: {115 rpId: publicKey.rp.id ?? window.location.hostname,116 rpName: publicKey.rp.name,117 userId,118 userName: publicKey.user.name,119 userDisplayName: publicKey.user.displayName,120 challenge,121 clientDataJSON: Array.from(clientDataJSON),122 origin: window.location.origin,123 pubKeyCredParams: publicKey.pubKeyCredParams,124 authenticatorSelection: publicKey.authenticatorSelection,125 timeout: publicKey.timeout,126 attestation: publicKey.attestation,127 },128 });129130 if (!response?.ok) {131 const error = response?.error;132 if (error === "user_cancelled") {133 throw new window.DOMException(134 "The operation either timed out or was not allowed.",135 "NotAllowedError",136 );137 }138 if (error === "pro_required") {139 window.dispatchEvent(140 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } }),141 );142 return originalCreate(options);143 }144 145 146 return originalCreate(options);147 }148149 const cred = validateCreateCredential(response.credential);150 return buildPublicKeyCredential(cred);151 } catch (err) {152 if (err instanceof DOMException && err.name === "NotAllowedError") throw err;153 154 return originalCreate(options);155 }156 });157 },158 pageCredentials,159 { defineAs: "create" },160 );161162 exportFunction(163 function patchedGet(options?: CredentialRequestOptions): Promise<Credential | null> {164 return pagePromise(async () => {165 166 const hints = (options?.publicKey as (PublicKeyCredentialRequestOptions & { hints?: string[] }) | undefined)?.hints;167 if (!options?.publicKey || options.mediation === "conditional" || hints?.[0] === "security-key") {168 return originalGet(options);169 }170171 try {172 const publicKey = options.publicKey;173 const rpId = publicKey.rpId ?? window.location.hostname;174 const allowCredentials = (publicKey.allowCredentials ?? []).map((c) => ({175 id: bufferToBase64url(toArrayBuffer(c.id)),176 type: c.type,177 }));178179 180 let has: { ok: boolean; error?: string; hasCredentials?: boolean } | undefined;181 for (let attempt = 0; attempt < 3; attempt++) {182 try {183 has = await sendMessage({184 type: "PASSKEY_HAS_CREDENTIALS",185 payload: { rpId, allowCredentials },186 });187 } catch {188 189 has = undefined;190 }191 if (has) break;192 if (attempt < 2) await new Promise<void>((resolve) => window.setTimeout(resolve, 150));193 }194 if (has?.error === "not_applicable" || (has?.ok && has.hasCredentials === false)) {195 return originalGet(options);196 }197 if (has?.error !== "vault_locked" && !(has?.ok && has.hasCredentials === true)) {198 throw new window.DOMException("Peach could not connect to its passkey provider. Try again.", "NotAllowedError");199 }200201 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));202 const clientDataJSON = encodeJsonBytes({203 type: "webauthn.get",204 challenge,205 origin: window.location.origin,206 crossOrigin: false,207 });208209 const response = await sendMessage({210 type: "PASSKEY_GET",211 payload: {212 rpId,213 challenge,214 clientDataJSON: Array.from(clientDataJSON),215 origin: window.location.origin,216 allowCredentials,217 userVerification: publicKey.userVerification,218 },219 });220221 if (!response?.ok) {222 const error = response?.error;223 if (error === "user_cancelled") {224 throw new window.DOMException(225 "The operation either timed out or was not allowed.",226 "NotAllowedError",227 );228 }229 if (error === "pro_required") {230 window.dispatchEvent(231 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } }),232 );233 return originalGet(options);234 }235 if (error === "no_credentials" || error === "not_applicable") return originalGet(options);236 throw new window.DOMException("Peach could not complete this passkey request. Try again.", "NotAllowedError");237 }238239 const cred = validateGetCredential(response.credential);240 return buildPublicKeyCredential(cred);241 } catch (err) {242 if (err instanceof DOMException && err.name === "NotAllowedError") throw err;243 throw new window.DOMException("Peach could not complete this passkey request. Try again.", "NotAllowedError");244 }245 });246 },247 pageCredentials,248 { defineAs: "get" },249 );250251 252253 const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;254255 function isBase64url(value: unknown): value is string {256 return typeof value === "string" && value.length > 0 && BASE64URL_RE.test(value);257 }258259 function isByteArray(value: unknown): value is number[] {260 return (261 Array.isArray(value) &&262 value.length > 0 &&263 value.every((b) => typeof b === "number" && Number.isInteger(b) && b >= 0 && b <= 255)264 );265 }266267 function validateCreateCredential(cred: unknown): CreateCredential {268 if (!cred || typeof cred !== "object") throw new ValidationError("missing credential");269 const c = cred as Record<string, unknown>;270 if (c.type !== "create") throw new ValidationError("wrong credential type");271 if (!isBase64url(c.id)) throw new ValidationError("id must be base64url");272 if (!isByteArray(c.clientDataJSON)) throw new ValidationError("clientDataJSON must be a byte array");273 if (!isBase64url(c.attestationObject)) throw new ValidationError("attestationObject must be base64url");274 if (!isBase64url(c.authenticatorData)) throw new ValidationError("authenticatorData must be base64url");275 if (!isBase64url(c.publicKey)) throw new ValidationError("publicKey must be base64url");276 if (c.algorithm !== -7 && c.algorithm !== -8) throw new ValidationError("unsupported algorithm");277 return c as unknown as CreateCredential;278 }279280 function validateGetCredential(cred: unknown): GetCredential {281 if (!cred || typeof cred !== "object") throw new ValidationError("missing credential");282 const c = cred as Record<string, unknown>;283 if (c.type !== "get") throw new ValidationError("wrong credential type");284 if (!isBase64url(c.id)) throw new ValidationError("id must be base64url");285 if (!isByteArray(c.clientDataJSON)) throw new ValidationError("clientDataJSON must be a byte array");286 if (!isBase64url(c.authenticatorData)) throw new ValidationError("authenticatorData must be base64url");287 if (!isBase64url(c.signature)) throw new ValidationError("signature must be base64url");288 if (c.userHandle !== undefined && c.userHandle !== null && !isBase64url(c.userHandle)) {289 throw new ValidationError("userHandle must be base64url");290 }291 return c as unknown as GetCredential;292 }293294 295296 class ValidationError extends Error {297 constructor(message: string) {298 super(message);299 this.name = "ValidationError";300 }301 }302303 interface CreateCredential {304 type: "create";305 id: string;306 clientDataJSON: number[];307 attestationObject: string;308 authenticatorData: string;309 publicKey: string;310 algorithm: -7 | -8;311 }312313 interface GetCredential {314 type: "get";315 id: string;316 clientDataJSON: number[];317 authenticatorData: string;318 signature: string;319 userHandle: string | null;320 }321322 function toArrayBuffer(view: BufferSource): ArrayBuffer {323 const bytes = ArrayBuffer.isView(view)324 ? new Uint8Array(view.buffer, view.byteOffset, view.byteLength)325 : new Uint8Array(view);326 return new Uint8Array(bytes).buffer;327 }328329 function bufferToBase64url(buffer: ArrayBuffer): string {330 const bytes = new Uint8Array(buffer);331 let str = "";332 for (let i = 0; i < bytes.length; i += 1) str += String.fromCharCode(bytes[i]!);333 return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");334 }335336 function base64urlToBuffer(value: string): ArrayBuffer {337 const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(338 Math.ceil(value.length / 4) * 4,339 "=",340 );341 const raw = atob(padded);342 const bytes = new Uint8Array(raw.length);343 for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);344 return bytes.buffer;345 }346347 function encodeJsonBytes(value: unknown): Uint8Array {348 return new TextEncoder().encode(JSON.stringify(value));349 }350351 352353354355356357358359360361362 function buildPublicKeyCredential(cred: CreateCredential | GetCredential): PublicKeyCredential {363 const credentialId = base64urlToBuffer(cred.id);364 const clientDataJSON = new Uint8Array(cred.clientDataJSON).buffer;365366 if (cred.type === "create") {367 const attestationObject = base64urlToBuffer(cred.attestationObject);368 const authenticatorData = base64urlToBuffer(cred.authenticatorData);369 const publicKey = base64urlToBuffer(cred.publicKey);370 const obj = {371 id: cred.id,372 rawId: credentialId,373 type: "public-key" as const,374 authenticatorAttachment: "platform" as const,375 response: {376 clientDataJSON,377 attestationObject,378 getTransports: () => cloneInto(["internal"], window),379 getAuthenticatorData: () => cloneInto(authenticatorData, window),380 getPublicKey: () => cloneInto(publicKey, window),381 getPublicKeyAlgorithm: () => cred.algorithm ?? -7,382 } as AuthenticatorAttestationResponse,383 getClientExtensionResults: () => cloneInto({}, window),384 toJSON: () => cloneInto({385 id: cred.id,386 rawId: cred.id,387 type: "public-key" as const,388 response: {389 clientDataJSON: bufferToBase64url(clientDataJSON),390 attestationObject: cred.attestationObject,391 authenticatorData: cred.authenticatorData,392 publicKey: cred.publicKey,393 publicKeyAlgorithm: cred.algorithm ?? -7,394 transports: ["internal"],395 },396 authenticatorAttachment: "platform" as const,397 clientExtensionResults: {},398 }, window),399 } as unknown as PublicKeyCredential;400 return cloneInto(obj, window, { cloneFunctions: true });401 }402403 const authenticatorData = base64urlToBuffer(cred.authenticatorData);404 const signature = base64urlToBuffer(cred.signature);405 const userHandle = cred.userHandle ? base64urlToBuffer(cred.userHandle) : null;406 const obj = {407 id: cred.id,408 rawId: credentialId,409 type: "public-key" as const,410 authenticatorAttachment: "platform" as const,411 response: {412 clientDataJSON,413 authenticatorData,414 signature,415 userHandle,416 } as AuthenticatorAssertionResponse,417 getClientExtensionResults: () => cloneInto({}, window),418 toJSON: () => cloneInto({419 id: cred.id,420 rawId: cred.id,421 type: "public-key" as const,422 response: {423 clientDataJSON: bufferToBase64url(clientDataJSON),424 authenticatorData: cred.authenticatorData,425 signature: cred.signature,426 userHandle: cred.userHandle ?? null,427 },428 authenticatorAttachment: "platform" as const,429 clientExtensionResults: {},430 }, window),431 } as unknown as PublicKeyCredential;432 return cloneInto(obj, window, { cloneFunctions: true });433 }434}435436installPeachPasskeyProxy();