UNPKG

@virtonetwork/authenticators-webauthn

Version:

An Authenticator compatible with KreivoPassSigner that uses the WebAuthn standard

253 lines (252 loc) 13.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebAuthn = exports.KREIVO_AUTHORITY_ID = exports.InMemoryCredentialsHandler = void 0; var substrate_bindings_1 = require("@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 */ var signer_1 = require("@virtonetwork/signer"); var in_memory_credentials_handler_ts_1 = require("./in-memory-credentials-handler.cjs"); Object.defineProperty(exports, "InMemoryCredentialsHandler", { enumerable: true, get: function () { return in_memory_credentials_handler_ts_1.InMemoryCredentialsHandler; } }); var types_ts_1 = require("./types.cjs"); /** Fixed authority id for Kreivo pass‑key attestors. */ exports.KREIVO_AUTHORITY_ID = substrate_bindings_1.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>} */ var WebAuthn = /** @class */ (function () { /** * 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}, * */ function WebAuthn(userId, getChallenge, handler, addressGenerator) { if (handler === void 0) { handler = new in_memory_credentials_handler_ts_1.InMemoryCredentialsHandler(); } if (addressGenerator === void 0) { addressGenerator = signer_1.kreivoPassDefaultAddressGenerator; } this.userId = userId; this.getChallenge = getChallenge; this.addressGenerator = addressGenerator; /** * SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused * for all WebAuthn operations. */ this.hashedUserId = new Uint8Array(32); var publicKeyCreateOptions = handler.publicKeyCreateOptions, publicKeyRequestOptions = handler.publicKeyRequestOptions, onCreatedCredentials = handler.onCreatedCredentials; 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. */ WebAuthn.prototype.setup = function () { return __awaiter(this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = this; return [4 /*yield*/, WebAuthn.getHashedUserId(this.userId)]; case 1: _a.hashedUserId = _b.sent(); return [2 /*return*/, this]; } }); }); }; WebAuthn.getHashedUserId = function (userId) { return __awaiter(this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = Uint8Array.bind; return [4 /*yield*/, crypto.subtle.digest("SHA-256", new TextEncoder().encode(userId))]; case 1: return [2 /*return*/, new (_a.apply(Uint8Array, [void 0, _b.sent()]))()]; } }); }); }; /** * 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. */ WebAuthn.prototype.getDeviceId = function (credentials) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, substrate_bindings_1.Binary.fromBytes((0, substrate_bindings_1.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. */ WebAuthn.prototype.register = function (blockNumber_1) { return __awaiter(this, arguments, void 0, function (blockNumber, displayName) { var challenge, credentials, _a, _b, response, clientDataJSON, publicKey; var _c, _d, _e; if (displayName === void 0) { displayName = this.userId; } return __generator(this, function (_f) { switch (_f.label) { case 0: return [4 /*yield*/, this.getChallenge(blockNumber, this.addressGenerator(this.hashedUserId))]; case 1: challenge = _f.sent(); _b = (_a = navigator.credentials).create; _c = {}; return [4 /*yield*/, this.getPublicKeyCreateOptions(challenge, { id: this.hashedUserId.buffer, name: this.userId, displayName: displayName, })]; case 2: return [4 /*yield*/, _b.apply(_a, [(_c.publicKey = _f.sent(), _c)])]; case 3: credentials = (_f.sent()); response = credentials.response; clientDataJSON = response.clientDataJSON; publicKey = response.getPublicKey(); if (!publicKey) { throw new Error("The credentials don't expose a public key. Please use another authenticator device."); } return [4 /*yield*/, this.onCreatedCredentials(this.userId, credentials)]; case 4: _f.sent(); _d = {}; _e = { authority_id: exports.KREIVO_AUTHORITY_ID }; return [4 /*yield*/, this.getDeviceId(credentials)]; case 5: return [2 /*return*/, (_d.meta = (_e.device_id = _f.sent(), _e.context = blockNumber, _e), _d.authenticator_data = substrate_bindings_1.Binary.fromBytes(new Uint8Array(response.getAuthenticatorData())), _d.client_data = substrate_bindings_1.Binary.fromBytes(new Uint8Array(clientDataJSON)), _d.public_key = substrate_bindings_1.Binary.fromBytes(new Uint8Array(publicKey)), _d)]; } }); }); }; /** * 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. */ WebAuthn.prototype.authenticate = function (context, xtc) { return __awaiter(this, void 0, void 0, function () { var challenge, credentials, _a, _b, _c, authenticatorData, clientDataJSON, signature; var _d, _e; return __generator(this, function (_f) { switch (_f.label) { case 0: return [4 /*yield*/, this.getChallenge(context, xtc)]; case 1: challenge = _f.sent(); _b = (_a = navigator.credentials).get; _d = {}; return [4 /*yield*/, this.getPublicKeyRequestOptions(this.userId, challenge)]; case 2: return [4 /*yield*/, _b.apply(_a, [(_d.publicKey = _f.sent(), _d)])]; case 3: credentials = (_f.sent()); _c = credentials.response, authenticatorData = _c.authenticatorData, clientDataJSON = _c.clientDataJSON, signature = _c.signature; _e = {}; return [4 /*yield*/, this.getDeviceId(credentials)]; case 4: return [2 /*return*/, (_e.deviceId = _f.sent(), _e.credentials = { tag: "WebAuthn", value: types_ts_1.Assertion.enc({ meta: { authority_id: exports.KREIVO_AUTHORITY_ID, user_id: substrate_bindings_1.Binary.fromBytes(this.hashedUserId), context: context, }, authenticator_data: substrate_bindings_1.Binary.fromBytes(new Uint8Array(authenticatorData)), client_data: substrate_bindings_1.Binary.fromBytes(new Uint8Array(clientDataJSON)), signature: substrate_bindings_1.Binary.fromBytes(new Uint8Array(signature)), }), }, _e)]; } }); }); }; return WebAuthn; }()); exports.WebAuthn = WebAuthn;