UNPKG

@virtonetwork/authenticators-webauthn

Version:

An Authenticator compatible with KreivoPassSigner that uses the WebAuthn standard

158 lines (157 loc) 7 kB
import { Binary, Blake2256 } from "@polkadot-api/substrate-bindings"; /** * WebAuthn pass‑key authenticator for Virto Network. * * Exposes a browser‑side implementation of {@link Authenticator} that creates, * stores, and uses WebAuthn resident credentials ("passkeys") while producing * SCALE‑encoded data structures understood by the Kreivo signer pallet. * * Responsibilities * ───────────────────────────────────────────────────────── * • Derive a deterministic `deviceId` from the raw credential id * • Emit `TAttestation<number>` during registration * • Emit `TPassAuthenticate` during authentication * • Never persist the credential mapping; that is delegated to the caller * * @module WebAuthn */ import { kreivoPassDefaultAddressGenerator, } from "@virtonetwork/signer"; import { InMemoryCredentialsHandler } from "./in-memory-credentials-handler.js"; import { Assertion } from "./types.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; addressGenerator; /** * SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused * for all WebAuthn operations. */ hashedUserId = new Uint8Array(32); getPublicKeyCreateOptions; getPublicKeyRequestOptions; onCreatedCredentials; /** * Creates a new WebAuthn helper. * * @param userId - Logical user identifier (e‑mail, DID, etc.).. * @param getChallenge - An implementation of the `generate` method used in the challenger, * @param credentialsHandler - An implementation of {@link CredentialsHandler}, * */ constructor(userId, getChallenge, handler = new InMemoryCredentialsHandler(), addressGenerator = kreivoPassDefaultAddressGenerator) { this.userId = userId; this.getChallenge = getChallenge; this.addressGenerator = addressGenerator; const { publicKeyCreateOptions, publicKeyRequestOptions, onCreatedCredentials, } = handler; this.getPublicKeyCreateOptions = publicKeyCreateOptions.bind(handler); this.getPublicKeyRequestOptions = publicKeyRequestOptions.bind(handler); this.onCreatedCredentials = onCreatedCredentials.bind(handler); } /** * 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(credentials) { return Binary.fromBytes(Blake2256(new Uint8Array(credentials.rawId))); } /** * 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) { const challenge = await this.getChallenge(blockNumber, this.addressGenerator(this.hashedUserId)); const credentials = (await navigator.credentials.create({ publicKey: await this.getPublicKeyCreateOptions(challenge, { id: this.hashedUserId.buffer, name: this.userId, displayName, }), })); const response = credentials.response; const { clientDataJSON } = response; // 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(credentials), context: blockNumber, }, authenticator_data: Binary.fromBytes(new Uint8Array(response.getAuthenticatorData())), 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 credentials = (await navigator.credentials.get({ publicKey: await this.getPublicKeyRequestOptions(this.userId, challenge), })); const { authenticatorData, clientDataJSON, signature } = credentials.response; return { deviceId: await this.getDeviceId(credentials), 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)), }), }, }; } }