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 exportFunction(90 async function patchedCreate(options?: CredentialCreationOptions): Promise<Credential | null> {91 if (!options?.publicKey) return originalCreate(options);9293 try {94 const publicKey = options.publicKey;95 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));96 const clientDataJSON = encodeJsonBytes({97 type: "webauthn.create",98 challenge,99 origin: window.location.origin,100 crossOrigin: false,101 });102103 const userId = bufferToBase64url(toArrayBuffer(publicKey.user.id));104105 const response = await sendMessage({106 type: "PASSKEY_CREATE",107 payload: {108 rpId: publicKey.rp.id ?? window.location.hostname,109 rpName: publicKey.rp.name,110 userId,111 userName: publicKey.user.name,112 userDisplayName: publicKey.user.displayName,113 challenge,114 clientDataJSON: Array.from(clientDataJSON),115 origin: window.location.origin,116 pubKeyCredParams: publicKey.pubKeyCredParams,117 authenticatorSelection: publicKey.authenticatorSelection,118 timeout: publicKey.timeout,119 attestation: publicKey.attestation,120 },121 });122123 if (!response?.ok) {124 const error = response?.error;125 if (error === "user_cancelled") {126 throw new DOMException(127 "The operation either timed out or was not allowed.",128 "NotAllowedError",129 );130 }131 if (error === "pro_required") {132 window.dispatchEvent(133 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } }),134 );135 return originalCreate(options);136 }137 138 139 return originalCreate(options);140 }141142 const cred = validateCreateCredential(response.credential);143 return buildPublicKeyCredential(cred);144 } catch (err) {145 if (err instanceof DOMException && err.name === "NotAllowedError") throw err;146 147 return originalCreate(options);148 }149 },150 pageCredentials,151 { defineAs: "create" },152 );153154 exportFunction(155 async function patchedGet(options?: CredentialRequestOptions): Promise<Credential | null> {156 if (!options?.publicKey) return originalGet(options);157158 try {159 const publicKey = options.publicKey;160 const rpId = publicKey.rpId ?? window.location.hostname;161 const allowCredentials = (publicKey.allowCredentials ?? []).map((c) => ({162 id: bufferToBase64url(toArrayBuffer(c.id)),163 type: c.type,164 }));165166 167 const has = await sendMessage({168 type: "PASSKEY_HAS_CREDENTIALS",169 payload: { rpId, allowCredentials },170 });171 if (!has?.ok || !has.hasCredentials) {172 return originalGet(options);173 }174175 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));176 const clientDataJSON = encodeJsonBytes({177 type: "webauthn.get",178 challenge,179 origin: window.location.origin,180 crossOrigin: false,181 });182183 const response = await sendMessage({184 type: "PASSKEY_GET",185 payload: {186 rpId,187 challenge,188 clientDataJSON: Array.from(clientDataJSON),189 origin: window.location.origin,190 allowCredentials,191 userVerification: publicKey.userVerification,192 },193 });194195 if (!response?.ok) {196 const error = response?.error;197 if (error === "user_cancelled") {198 throw new DOMException(199 "The operation either timed out or was not allowed.",200 "NotAllowedError",201 );202 }203 if (error === "pro_required") {204 window.dispatchEvent(205 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } }),206 );207 return originalGet(options);208 }209 return originalGet(options);210 }211212 const cred = validateGetCredential(response.credential);213 return buildPublicKeyCredential(cred);214 } catch (err) {215 if (err instanceof DOMException && err.name === "NotAllowedError") throw err;216 return originalGet(options);217 }218 },219 pageCredentials,220 { defineAs: "get" },221 );222223 224225 const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;226227 function isBase64url(value: unknown): value is string {228 return typeof value === "string" && value.length > 0 && BASE64URL_RE.test(value);229 }230231 function isByteArray(value: unknown): value is number[] {232 return (233 Array.isArray(value) &&234 value.length > 0 &&235 value.every((b) => typeof b === "number" && Number.isInteger(b) && b >= 0 && b <= 255)236 );237 }238239 function validateCreateCredential(cred: unknown): CreateCredential {240 if (!cred || typeof cred !== "object") throw new ValidationError("missing credential");241 const c = cred as Record<string, unknown>;242 if (c.type !== "create") throw new ValidationError("wrong credential type");243 if (!isBase64url(c.id)) throw new ValidationError("id must be base64url");244 if (!isByteArray(c.clientDataJSON)) throw new ValidationError("clientDataJSON must be a byte array");245 if (!isBase64url(c.attestationObject)) throw new ValidationError("attestationObject must be base64url");246 if (!isBase64url(c.authenticatorData)) throw new ValidationError("authenticatorData must be base64url");247 if (!isBase64url(c.publicKey)) throw new ValidationError("publicKey must be base64url");248 if (c.algorithm !== -7 && c.algorithm !== -8) throw new ValidationError("unsupported algorithm");249 return c as unknown as CreateCredential;250 }251252 function validateGetCredential(cred: unknown): GetCredential {253 if (!cred || typeof cred !== "object") throw new ValidationError("missing credential");254 const c = cred as Record<string, unknown>;255 if (c.type !== "get") throw new ValidationError("wrong credential type");256 if (!isBase64url(c.id)) throw new ValidationError("id must be base64url");257 if (!isByteArray(c.clientDataJSON)) throw new ValidationError("clientDataJSON must be a byte array");258 if (!isBase64url(c.authenticatorData)) throw new ValidationError("authenticatorData must be base64url");259 if (!isBase64url(c.signature)) throw new ValidationError("signature must be base64url");260 if (c.userHandle !== undefined && c.userHandle !== null && !isBase64url(c.userHandle)) {261 throw new ValidationError("userHandle must be base64url");262 }263 return c as unknown as GetCredential;264 }265266 267268 class ValidationError extends Error {269 constructor(message: string) {270 super(message);271 this.name = "ValidationError";272 }273 }274275 interface CreateCredential {276 type: "create";277 id: string;278 clientDataJSON: number[];279 attestationObject: string;280 authenticatorData: string;281 publicKey: string;282 algorithm: -7 | -8;283 }284285 interface GetCredential {286 type: "get";287 id: string;288 clientDataJSON: number[];289 authenticatorData: string;290 signature: string;291 userHandle: string | null;292 }293294 function toArrayBuffer(view: BufferSource): ArrayBuffer {295 if (view instanceof ArrayBuffer) return view;296 const u8 = view as ArrayBufferView;297 const out = new Uint8Array(u8.byteLength);298 out.set(new Uint8Array(u8.buffer, u8.byteOffset, u8.byteLength));299 return out.buffer;300 }301302 function bufferToBase64url(buffer: ArrayBuffer): string {303 const bytes = new Uint8Array(buffer);304 let str = "";305 for (let i = 0; i < bytes.length; i += 1) str += String.fromCharCode(bytes[i]!);306 return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");307 }308309 function base64urlToBuffer(value: string): ArrayBuffer {310 const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(311 Math.ceil(value.length / 4) * 4,312 "=",313 );314 const raw = atob(padded);315 const bytes = new Uint8Array(raw.length);316 for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);317 return bytes.buffer;318 }319320 function encodeJsonBytes(value: unknown): Uint8Array {321 return new TextEncoder().encode(JSON.stringify(value));322 }323324 325326327328329330331332333334335 function buildPublicKeyCredential(cred: CreateCredential | GetCredential): PublicKeyCredential {336 const credentialId = base64urlToBuffer(cred.id);337 const clientDataJSON = new Uint8Array(cred.clientDataJSON).buffer;338339 if (cred.type === "create") {340 const attestationObject = base64urlToBuffer(cred.attestationObject);341 const authenticatorData = base64urlToBuffer(cred.authenticatorData);342 const publicKey = base64urlToBuffer(cred.publicKey);343 const obj = {344 id: cred.id,345 rawId: credentialId,346 type: "public-key" as const,347 authenticatorAttachment: "platform" as const,348 response: {349 clientDataJSON,350 attestationObject,351 getTransports: () => ["internal"],352 getAuthenticatorData: () => authenticatorData,353 getPublicKey: () => publicKey,354 getPublicKeyAlgorithm: () => cred.algorithm ?? -7,355 } as AuthenticatorAttestationResponse,356 getClientExtensionResults: () => ({}),357 toJSON: () => ({358 id: cred.id,359 rawId: cred.id,360 type: "public-key" as const,361 response: {362 clientDataJSON: bufferToBase64url(clientDataJSON),363 attestationObject: cred.attestationObject,364 authenticatorData: cred.authenticatorData,365 publicKey: cred.publicKey,366 publicKeyAlgorithm: cred.algorithm ?? -7,367 transports: ["internal"],368 },369 authenticatorAttachment: "platform" as const,370 clientExtensionResults: {},371 }),372 } as unknown as PublicKeyCredential;373 return cloneInto(obj, window, { cloneFunctions: true });374 }375376 const authenticatorData = base64urlToBuffer(cred.authenticatorData);377 const signature = base64urlToBuffer(cred.signature);378 const userHandle = cred.userHandle ? base64urlToBuffer(cred.userHandle) : null;379 const obj = {380 id: cred.id,381 rawId: credentialId,382 type: "public-key" as const,383 authenticatorAttachment: "platform" as const,384 response: {385 clientDataJSON,386 authenticatorData,387 signature,388 userHandle,389 } as AuthenticatorAssertionResponse,390 getClientExtensionResults: () => ({}),391 toJSON: () => ({392 id: cred.id,393 rawId: cred.id,394 type: "public-key" as const,395 response: {396 clientDataJSON: bufferToBase64url(clientDataJSON),397 authenticatorData: cred.authenticatorData,398 signature: cred.signature,399 userHandle: cred.userHandle ?? null,400 },401 authenticatorAttachment: "platform" as const,402 clientExtensionResults: {},403 }),404 } as unknown as PublicKeyCredential;405 return cloneInto(obj, window, { cloneFunctions: true });406 }407}408409installPeachPasskeyProxy();