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