import * as Base64 from '../core/Base64.js' import * as Bytes from '../core/Bytes.js' import * as Errors from '../core/Errors.js' import * as Hash from '../core/Hash.js' import * as Hex from '../core/Hex.js' import type { OneOf } from '../core/internal/types.js' import * as internal from '../core/internal/webauthn.js' import * as P256 from '../core/P256.js' import type * as PublicKey from '../core/PublicKey.js' import * as Signature from '../core/Signature.js' import { getAuthenticatorData, getClientDataJSON } from './Authenticator.js' import type * as Credential_ from './Credential.js' import { base64UrlOptions, bufferSourceToBytes, bytesToArrayBuffer, deserializeExtensions, responseKeys, serializeExtensions, } from './internal/utils.js' import type * as Types from './Types.js' /** Response from a WebAuthn authentication ceremony. */ export type Response = { id: string metadata: Credential_.SignMetadata raw: Types.PublicKeyCredential signature: serialized extends true ? Hex.Hex : Signature.Signature } /** * Deserializes credential request options that can be passed to * `navigator.credentials.get()`. * * @example * ```ts twoslash * import { Authentication } from 'ox/webauthn' * * const options = Authentication.getOptions({ * challenge: '0xdeadbeef', * }) * const serialized = Authentication.serializeOptions(options) * * // ... send to server and back ... * * const deserialized = Authentication.deserializeOptions(serialized) // [!code focus] * const credential = await window.navigator.credentials.get(deserialized) * ``` * * @param options - The serialized credential request options. * @returns The deserialized credential request options. */ export function deserializeOptions( options: Types.CredentialRequestOptions, ): Types.CredentialRequestOptions { const { publicKey, ...rest } = options if (!publicKey) return { ...rest } const { allowCredentials, challenge, extensions, ...publicKeyRest } = publicKey return { ...rest, publicKey: { ...publicKeyRest, challenge: Bytes.fromHex(challenge), ...(allowCredentials && { allowCredentials: allowCredentials.map(({ id, ...rest }) => ({ ...rest, id: Base64.toBytes(id), })), }), ...(extensions && { extensions: deserializeExtensions(extensions), }), }, } } export declare namespace deserializeOptions { type ErrorType = Base64.toBytes.ErrorType | Errors.GlobalErrorType } /** * Deserializes a serialized authentication response. * * @example * ```ts twoslash * import { Authentication } from 'ox/webauthn' * * const response = Authentication.deserializeResponse({ // [!code focus] * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus] * metadata: { // [!code focus] * authenticatorData: '0x49960de5...', // [!code focus] * clientDataJSON: '{"type":"webauthn.get",...}', // [!code focus] * challengeIndex: 23, // [!code focus] * typeIndex: 1, // [!code focus] * userVerificationRequired: true, // [!code focus] * }, // [!code focus] * raw: { // [!code focus] * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus] * type: 'public-key', // [!code focus] * authenticatorAttachment: 'platform', // [!code focus] * rawId: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus] * response: { clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0' }, // [!code focus] * }, // [!code focus] * signature: '0x...', // [!code focus] * }) // [!code focus] * ``` * * @param response - The serialized authentication response. * @returns The deserialized authentication response. */ export function deserializeResponse(response: Response): Response { const { id, metadata, raw, signature } = response const rawResponse: Record = {} for (const [key, value] of Object.entries(raw.response)) rawResponse[key] = bytesToArrayBuffer(Base64.toBytes(value)) return { id, metadata, raw: { id: raw.id, type: raw.type, authenticatorAttachment: raw.authenticatorAttachment, rawId: bytesToArrayBuffer(Base64.toBytes(raw.rawId)), response: rawResponse as unknown as Types.AuthenticatorResponse, getClientExtensionResults: () => ({}), }, signature: Signature.from(signature), } } export declare namespace deserializeResponse { type ErrorType = | Base64.toBytes.ErrorType | Signature.from.ErrorType | Errors.GlobalErrorType } /** * Returns the request options to sign a challenge with the Web Authentication API. * * @example * ```ts twoslash * import { Authentication } from 'ox/webauthn' * * const options = Authentication.getOptions({ * challenge: '0xdeadbeef', * }) * * const credential = await window.navigator.credentials.get(options) * ``` * * @param options - Options. * @returns The credential request options. */ export function getOptions( options: getOptions.Options, ): Types.CredentialRequestOptions { const { credentialId, challenge, extensions, rpId = window.location.hostname, userVerification = 'required', } = options return { publicKey: { ...(credentialId ? { allowCredentials: Array.isArray(credentialId) ? credentialId.map((id) => ({ id: Base64.toBytes(id), type: 'public-key', })) : [ { id: Base64.toBytes(credentialId), type: 'public-key', }, ], } : {}), challenge: Bytes.fromHex(challenge), ...(extensions && { extensions }), rpId, userVerification, }, } } export declare namespace getOptions { type Options = { /** The credential ID to use. */ credentialId?: string | string[] | undefined /** The challenge to sign. */ challenge: Hex.Hex /** List of Web Authentication API credentials to use during creation or authentication. */ extensions?: | Types.PublicKeyCredentialRequestOptions['extensions'] | undefined /** The relying party identifier to use. */ rpId?: Types.PublicKeyCredentialRequestOptions['rpId'] | undefined /** The user verification requirement. */ userVerification?: | Types.PublicKeyCredentialRequestOptions['userVerification'] | undefined } type ErrorType = | Bytes.fromHex.ErrorType | Base64.toBytes.ErrorType | Errors.GlobalErrorType } /** * Constructs the final digest that was signed and computed by the authenticator. This payload includes * the cryptographic `challenge`, as well as authenticator metadata (`authenticatorData` + `clientDataJSON`). * This value can be also used with raw P256 verification (such as `P256.verify` or * `WebCryptoP256.verify`). * * :::warning * * This function is mainly for testing purposes or for manually constructing * signing payloads. In most cases you will not need this function and * instead use `Authentication.sign`. * * ::: * * @example * ```ts twoslash * import { Authentication } from 'ox/webauthn' * import { WebCryptoP256 } from 'ox' * * const { metadata, payload } = Authentication.getSignPayload({ // [!code focus] * challenge: '0xdeadbeef', // [!code focus] * }) // [!code focus] * * const { publicKey, privateKey } = await WebCryptoP256.createKeyPair() * * const signature = await WebCryptoP256.sign({ * payload, * privateKey, * }) * ``` * * @param options - Options to construct the signing payload. * @returns The signing payload. */ export function getSignPayload( options: getSignPayload.Options, ): getSignPayload.ReturnType { const { challenge, crossOrigin, extraClientData, flag, origin, rpId, signCount, userVerification = 'required', } = options const authenticatorData = getAuthenticatorData({ flag, rpId, signCount, }) const clientDataJSON = getClientDataJSON({ challenge, crossOrigin, extraClientData, origin, }) const clientDataJSONHash = Hash.sha256(Hex.fromString(clientDataJSON)) const challengeIndex = clientDataJSON.indexOf('"challenge"') const typeIndex = clientDataJSON.indexOf('"type"') const metadata = { authenticatorData, clientDataJSON, challengeIndex, typeIndex, userVerificationRequired: userVerification === 'required', } const payload = Hex.concat(authenticatorData, clientDataJSONHash) return { metadata, payload } } export declare namespace getSignPayload { type Options = { /** The challenge to sign. */ challenge: Hex.Hex /** If set to `true`, it means that the calling context is an `