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 if (!options?.publicKey || options.mediation === "conditional") return originalGet(options);167168 try {169 const publicKey = options.publicKey;170 const rpId = publicKey.rpId ?? window.location.hostname;171 const allowCredentials = (publicKey.allowCredentials ?? []).map((c) => ({172 id: bufferToBase64url(toArrayBuffer(c.id)),173 type: c.type,174 }));175176 177 const has = await sendMessage({178 type: "PASSKEY_HAS_CREDENTIALS",179 payload: { rpId, allowCredentials },180 });181 if (!has?.ok || !has.hasCredentials) {182 return originalGet(options);183 }184185 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));186 const clientDataJSON = encodeJsonBytes({187 type: "webauthn.get",188 challenge,189 origin: window.location.origin,190 crossOrigin: false,191 });192193 const response = await sendMessage({194 type: "PASSKEY_GET",195 payload: {196 rpId,197 challenge,198 clientDataJSON: Array.from(clientDataJSON),199 origin: window.location.origin,200 allowCredentials,201 userVerification: publicKey.userVerification,202 },203 });204205 if (!response?.ok) {206 const error = response?.error;207 if (error === "user_cancelled") {208 throw new window.DOMException(209 "The operation either timed out or was not allowed.",210 "NotAllowedError",211 );212 }213 if (error === "pro_required") {214 window.dispatchEvent(215 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } }),216 );217 return originalGet(options);218 }219 return originalGet(options);220 }221222 const cred = validateGetCredential(response.credential);223 return buildPublicKeyCredential(cred);224 } catch (err) {225 if (err instanceof DOMException && err.name === "NotAllowedError") throw err;226 return originalGet(options);227 }228 });229 },230 pageCredentials,231 { defineAs: "get" },232 );233234 235236 const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;237238 function isBase64url(value: unknown): value is string {239 return typeof value === "string" && value.length > 0 && BASE64URL_RE.test(value);240 }241242 function isByteArray(value: unknown): value is number[] {243 return (244 Array.isArray(value) &&245 value.length > 0 &&246 value.every((b) => typeof b === "number" && Number.isInteger(b) && b >= 0 && b <= 255)247 );248 }249250 function validateCreateCredential(cred: unknown): CreateCredential {251 if (!cred || typeof cred !== "object") throw new ValidationError("missing credential");252 const c = cred as Record<string, unknown>;253 if (c.type !== "create") throw new ValidationError("wrong credential type");254 if (!isBase64url(c.id)) throw new ValidationError("id must be base64url");255 if (!isByteArray(c.clientDataJSON)) throw new ValidationError("clientDataJSON must be a byte array");256 if (!isBase64url(c.attestationObject)) throw new ValidationError("attestationObject must be base64url");257 if (!isBase64url(c.authenticatorData)) throw new ValidationError("authenticatorData must be base64url");258 if (!isBase64url(c.publicKey)) throw new ValidationError("publicKey must be base64url");259 if (c.algorithm !== -7 && c.algorithm !== -8) throw new ValidationError("unsupported algorithm");260 return c as unknown as CreateCredential;261 }262263 function validateGetCredential(cred: unknown): GetCredential {264 if (!cred || typeof cred !== "object") throw new ValidationError("missing credential");265 const c = cred as Record<string, unknown>;266 if (c.type !== "get") throw new ValidationError("wrong credential type");267 if (!isBase64url(c.id)) throw new ValidationError("id must be base64url");268 if (!isByteArray(c.clientDataJSON)) throw new ValidationError("clientDataJSON must be a byte array");269 if (!isBase64url(c.authenticatorData)) throw new ValidationError("authenticatorData must be base64url");270 if (!isBase64url(c.signature)) throw new ValidationError("signature must be base64url");271 if (c.userHandle !== undefined && c.userHandle !== null && !isBase64url(c.userHandle)) {272 throw new ValidationError("userHandle must be base64url");273 }274 return c as unknown as GetCredential;275 }276277 278279 class ValidationError extends Error {280 constructor(message: string) {281 super(message);282 this.name = "ValidationError";283 }284 }285286 interface CreateCredential {287 type: "create";288 id: string;289 clientDataJSON: number[];290 attestationObject: string;291 authenticatorData: string;292 publicKey: string;293 algorithm: -7 | -8;294 }295296 interface GetCredential {297 type: "get";298 id: string;299 clientDataJSON: number[];300 authenticatorData: string;301 signature: string;302 userHandle: string | null;303 }304305 function toArrayBuffer(view: BufferSource): ArrayBuffer {306 const bytes = ArrayBuffer.isView(view)307 ? new Uint8Array(view.buffer, view.byteOffset, view.byteLength)308 : new Uint8Array(view);309 return new Uint8Array(bytes).buffer;310 }311312 function bufferToBase64url(buffer: ArrayBuffer): string {313 const bytes = new Uint8Array(buffer);314 let str = "";315 for (let i = 0; i < bytes.length; i += 1) str += String.fromCharCode(bytes[i]!);316 return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");317 }318319 function base64urlToBuffer(value: string): ArrayBuffer {320 const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(321 Math.ceil(value.length / 4) * 4,322 "=",323 );324 const raw = atob(padded);325 const bytes = new Uint8Array(raw.length);326 for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);327 return bytes.buffer;328 }329330 function encodeJsonBytes(value: unknown): Uint8Array {331 return new TextEncoder().encode(JSON.stringify(value));332 }333334 335336337338339340341342343344345 function buildPublicKeyCredential(cred: CreateCredential | GetCredential): PublicKeyCredential {346 const credentialId = base64urlToBuffer(cred.id);347 const clientDataJSON = new Uint8Array(cred.clientDataJSON).buffer;348349 if (cred.type === "create") {350 const attestationObject = base64urlToBuffer(cred.attestationObject);351 const authenticatorData = base64urlToBuffer(cred.authenticatorData);352 const publicKey = base64urlToBuffer(cred.publicKey);353 const obj = {354 id: cred.id,355 rawId: credentialId,356 type: "public-key" as const,357 authenticatorAttachment: "platform" as const,358 response: {359 clientDataJSON,360 attestationObject,361 getTransports: () => cloneInto(["internal"], window),362 getAuthenticatorData: () => cloneInto(authenticatorData, window),363 getPublicKey: () => cloneInto(publicKey, window),364 getPublicKeyAlgorithm: () => cred.algorithm ?? -7,365 } as AuthenticatorAttestationResponse,366 getClientExtensionResults: () => cloneInto({}, window),367 toJSON: () => cloneInto({368 id: cred.id,369 rawId: cred.id,370 type: "public-key" as const,371 response: {372 clientDataJSON: bufferToBase64url(clientDataJSON),373 attestationObject: cred.attestationObject,374 authenticatorData: cred.authenticatorData,375 publicKey: cred.publicKey,376 publicKeyAlgorithm: cred.algorithm ?? -7,377 transports: ["internal"],378 },379 authenticatorAttachment: "platform" as const,380 clientExtensionResults: {},381 }, window),382 } as unknown as PublicKeyCredential;383 return cloneInto(obj, window, { cloneFunctions: true });384 }385386 const authenticatorData = base64urlToBuffer(cred.authenticatorData);387 const signature = base64urlToBuffer(cred.signature);388 const userHandle = cred.userHandle ? base64urlToBuffer(cred.userHandle) : null;389 const obj = {390 id: cred.id,391 rawId: credentialId,392 type: "public-key" as const,393 authenticatorAttachment: "platform" as const,394 response: {395 clientDataJSON,396 authenticatorData,397 signature,398 userHandle,399 } as AuthenticatorAssertionResponse,400 getClientExtensionResults: () => cloneInto({}, window),401 toJSON: () => cloneInto({402 id: cred.id,403 rawId: cred.id,404 type: "public-key" as const,405 response: {406 clientDataJSON: bufferToBase64url(clientDataJSON),407 authenticatorData: cred.authenticatorData,408 signature: cred.signature,409 userHandle: cred.userHandle ?? null,410 },411 authenticatorAttachment: "platform" as const,412 clientExtensionResults: {},413 }, window),414 } as unknown as PublicKeyCredential;415 return cloneInto(obj, window, { cloneFunctions: true });416 }417}418419installPeachPasskeyProxy();