ox
Version:
Ethereum Standard Library
150 lines (140 loc) • 4.13 kB
text/typescript
import * as Base64 from '../../core/Base64.js'
import type * as Types from '../Types.js'
/** @internal */
export const base64UrlOptions = { url: true, pad: false } as const
/** @internal */
export const responseKeys = [
'attestationObject',
'authenticatorData',
'clientDataJSON',
'signature',
'userHandle',
] as const
/** @internal */
export function bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength,
) as ArrayBuffer
}
/** @internal */
export function bufferSourceToBytes(source: Types.BufferSource): Uint8Array {
if (source instanceof Uint8Array) return source
if (source instanceof ArrayBuffer) return new Uint8Array(source)
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength)
}
/**
* Parsed authenticator data per W3C WebAuthn Level 2 §6.1.
*
* @internal
*/
export type ParsedAuthenticatorData = {
/** Raw authenticator data bytes (zero-copy view). */
bytes: Uint8Array
/** SHA-256 hash of the relying party ID (32 bytes). */
rpIdHash: Uint8Array
/** Authenticator flags byte. */
flags: number
/** User Present flag. */
up: boolean
/** User Verified flag. */
uv: boolean
/** Backup Eligibility flag. */
be: boolean
/** Backup State flag. */
bs: boolean
/** Attested credential data flag. */
at: boolean
/** Extension data flag. */
ed: boolean
/** Signature counter (4-byte big-endian). */
signCount: number
}
/**
* Parses the fixed 37-byte header of authenticator data into rpIdHash, flags
* and signature counter. Variable-length tail (attested credential data,
* extensions) is the caller's responsibility.
*
* @internal
*/
export function parseAuthenticatorData(
authenticatorData: Uint8Array,
): ParsedAuthenticatorData | undefined {
if (authenticatorData.length < 37) return undefined
const flags = authenticatorData[32]!
return {
bytes: authenticatorData,
rpIdHash: authenticatorData.subarray(0, 32),
flags,
up: (flags & 0x01) === 0x01,
uv: (flags & 0x04) === 0x04,
be: (flags & 0x08) === 0x08,
bs: (flags & 0x10) === 0x10,
at: (flags & 0x40) === 0x40,
ed: (flags & 0x80) === 0x80,
signCount:
((authenticatorData[33]! << 24) |
(authenticatorData[34]! << 16) |
(authenticatorData[35]! << 8) |
authenticatorData[36]!) >>>
0,
}
}
/**
* Serializes the response fields of a WebAuthn `AuthenticatorResponse`-like
* object into a base64url-encoded record. Falls back to getter methods
* (e.g. `getAuthenticatorData()`) for browsers and passkey providers that
* expose response fields only via getters.
*
* @internal
*/
export function serializeResponseFields(
response: Record<string, unknown>,
): Record<string, string> {
const out: Record<string, string> = {}
for (const key of responseKeys) {
let value = response[key]
// Some providers expose response fields only via getter methods
// (e.g. `getAuthenticatorData()`).
if (!(value instanceof ArrayBuffer)) {
const getter = `get${key[0]!.toUpperCase()}${key.slice(1)}`
const fn = response[getter]
if (typeof fn === 'function') value = (fn as () => unknown).call(response)
}
if (value instanceof ArrayBuffer)
out[key] = Base64.fromBytes(new Uint8Array(value), base64UrlOptions)
}
return out
}
/** @internal */
export function serializeExtensions(
extensions: Types.AuthenticationExtensionsClientInputs,
): Types.AuthenticationExtensionsClientInputs<true> {
const { prf, ...rest } = extensions
return {
...rest,
...(prf && {
prf: {
eval: {
first: Base64.fromBytes(prf.eval.first, base64UrlOptions),
},
},
}),
}
}
/** @internal */
export function deserializeExtensions(
extensions: Types.AuthenticationExtensionsClientInputs<true>,
): Types.AuthenticationExtensionsClientInputs {
const { prf, ...rest } = extensions
return {
...rest,
...(prf && {
prf: {
eval: {
first: Base64.toBytes(prf.eval.first),
},
},
}),
}
}