UNPKG

@virtonetwork/authenticators-webauthn

Version:

An Authenticator compatible with KreivoPassSigner that uses the WebAuthn standard

146 lines (145 loc) 6.26 kB
import { Binary, Blake2256 } from "@polkadot-api/substrate-bindings"; import { Assertion } from "./types.js"; import { InMemoryCredentialsHandler } from "./in-memory-credentials-handler.js"; export { InMemoryCredentialsHandler }; /** Fixed authority id for Kreivo pass‑key attestors. */ export const KREIVO_AUTHORITY_ID = Binary.fromText("kreivo_p".padEnd(32, "\0")); /** * Browser‑side Authenticator that wraps the WebAuthn API. * * The generic type parameter `<number>` indicates that the **context** * carried inside attestations and assertions is the block number that * originated the challenge. * * @implements {Authenticator<number>} */ export class WebAuthn { userId; getChallenge; /** * SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused * for all WebAuthn operations. */ hashedUserId = new Uint8Array(32); credentialId; getPublicKeyCreateOptions; getPublicKeyRequestOptions; onCreatedCredentials; /** * Creates a new WebAuthn helper. * * @param userId - Logical user identifier (e‑mail, DID, etc.). * @param [credentialId] - Raw credential id obtained from a previous * registration flow; omit it if the user must enrol a new pass‑key. */ constructor(userId, getChallenge, { publicKeyCreateOptions, publicKeyRequestOptions, onCreatedCredentials, } = new InMemoryCredentialsHandler()) { this.userId = userId; this.getChallenge = getChallenge; this.getPublicKeyCreateOptions = publicKeyCreateOptions; this.getPublicKeyRequestOptions = publicKeyRequestOptions; this.onCreatedCredentials = onCreatedCredentials; } /** * Pre‑computes {@link hashedUserId}. * * Must be awaited **once** before any other interaction. * Returns `this` for fluent chaining. */ async setup() { this.hashedUserId = await WebAuthn.getHashedUserId(this.userId); return this; } static async getHashedUserId(userId) { return new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(userId))); } /** * Deterministic identifier of the hardware/software authenticator * (`deviceId = Blake2‑256(credentialId)`). * * @returns DeviceId suitable for on‑chain storage. * @throws Error If this instance does not yet know a credential id. */ async getDeviceId() { if (!this.credentialId) { throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()"); } return Binary.fromBytes(Blake2256(this.credentialId)); } /** * Registers a **new** resident credential (pass‑key) with the user’s * authenticator and returns a SCALE‑ready attestation. * * @param blockNumber - The number of the block whose hash seeds the challenge. * @param blockHash - The block hash used to derive a deterministic challenge. * @param [displayName=this.userId] - Friendly name shown by the authenticator. * * @throws Error If this instance already has a credential id. * @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object. */ async register(blockNumber, displayName = this.userId) { if (this.credentialId) { throw new Error("Already have a credentialId; no need to register"); } const challenge = await this.getChallenge(blockNumber, new Uint8Array([])); const credentials = (await navigator.credentials.create({ publicKey: await this.getPublicKeyCreateOptions(challenge, { id: this.hashedUserId, name: this.userId, displayName, }), })); const response = credentials.response; const { attestationObject, clientDataJSON } = response; // Save raw credential id for future auth calls this.credentialId = new Uint8Array(credentials.rawId); // Ensure publicKey is obtained in the registration process. const publicKey = response.getPublicKey(); if (!publicKey) { throw new Error("The credentials don't expose a public key. Please use another authenticator device."); } await this.onCreatedCredentials(this.userId, credentials); return { meta: { authority_id: KREIVO_AUTHORITY_ID, device_id: await this.getDeviceId(), context: blockNumber, }, authenticator_data: Binary.fromBytes(new Uint8Array(attestationObject)), client_data: Binary.fromBytes(new Uint8Array(clientDataJSON)), public_key: Binary.fromBytes(new Uint8Array(publicKey)), }; } /** * Signs an arbitrary challenge with the pass‑key and produces a * {@link TPassAuthenticate} payload understood by `PassSigner`. * * @param challenge - 32‑byte buffer supplied by the runtime. * @param context - Block number (or any numeric context expected by the pallet). * * @returns SCALE‑encoded authentication payload. * @throws Error If no credential id is available. */ async authenticate(context, xtc) { const challenge = await this.getChallenge(context, xtc); const cred = (await navigator.credentials.get({ publicKey: await this.getPublicKeyRequestOptions(this.userId, challenge), })); const { authenticatorData, clientDataJSON, signature } = cred.response; return { deviceId: await this.getDeviceId(), credentials: { tag: "WebAuthn", value: Assertion.enc({ meta: { authority_id: KREIVO_AUTHORITY_ID, user_id: Binary.fromBytes(this.hashedUserId), context, }, authenticator_data: Binary.fromBytes(new Uint8Array(authenticatorData)), client_data: Binary.fromBytes(new Uint8Array(clientDataJSON)), signature: Binary.fromBytes(new Uint8Array(signature)), }), }, }; } }