UNPKG

@abstraxn/passkey-manager

Version:

@abstraxn/passkey-manager is an npm package that provides a set of utilities and classes for creating and managing WebAuthn passkeys, extracting signatures, and handling local storage formats. The package is designed with an object-oriented approach, maki

218 lines 8.97 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractClientDataFields = exports.extractSignature = exports.isLocalStoragePasskey = exports.toLocalStorageFormatUsername = exports.toLocalStorageFormat = exports.getPassKey = exports.createPasskey = void 0; const buffer_1 = require("buffer"); const ethers_1 = require("ethers"); async function isSupportedByBrowser() { return (window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function"); } /** * Creates a passkey for signing. * * @returns A promise that resolves to a PasskeyCredentialWithPubkeyCoordinates object, which includes the passkey credential information and its public key coordinates. * @throws Throws an error if the passkey generation fails or if the credential received is null. */ async function createPasskey(username) { const isSupported = await isSupportedByBrowser(); if (!isSupported) { throw new Error('Your device is not supported for passkeys'); } // Generate a passkey credential using WebAuthn API // const passkeyCredential = (await navigator.credentials.create({ // publicKey: { // pubKeyCredParams: [ // { type: 'public-key', alg: -7 }, // ES256 // { type: 'public-key', alg: -257 }, // RS256 // ], // challenge: crypto.getRandomValues(new Uint8Array(32)), // rp: { // name: 'Abstraxn Wallet', // }, // user: { // displayName: username, // id: crypto.getRandomValues(new Uint8Array(32)), // name: username, // }, // timeout: 60000, // attestation: 'none', // // authenticatorSelection: { // // // authenticatorAttachment: 'cross-platform', // // userVerification: 'discouraged', // // }, // }, // })) as PasskeyCredential | null const passkeyCredential = await navigator.credentials.create({ publicKey: { pubKeyCredParams: [ { alg: -7, type: "public-key" }, { alg: -257, type: "public-key" }, // RS256 (for older Windows Hello and others) ], challenge: crypto.getRandomValues(new Uint8Array(32)), rp: { name: 'Abstraxn SmartAccount', }, user: { displayName: username, id: crypto.getRandomValues(new Uint8Array(32)), name: username, }, timeout: 60000, authenticatorSelection: { userVerification: 'preferred', residentKey: 'preferred', }, attestation: 'direct' } }); if (!passkeyCredential) { throw new Error('Failed to generate passkey. Received null as a credential'); } // Import the public key to later export it to get the XY coordinates try { const key = await crypto.subtle.importKey('spki', passkeyCredential.response.getPublicKey(), { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } }, true, ['verify']); // Export the public key in JWK format and extract XY coordinates const exportedKeyWithXYCoordinates = await crypto.subtle.exportKey('jwk', key); if (!exportedKeyWithXYCoordinates.x || !exportedKeyWithXYCoordinates.y) { throw new Error('Failed to retrieve x and y coordinates'); } // Create a PasskeyCredentialWithPubkeyCoordinates object const passkeyWithCoordinates = Object.assign(passkeyCredential, { pubkeyCoordinates: { x: BigInt('0x' + buffer_1.Buffer.from(exportedKeyWithXYCoordinates.x, 'base64').toString('hex')), y: BigInt('0x' + buffer_1.Buffer.from(exportedKeyWithXYCoordinates.y, 'base64').toString('hex')), }, }); return passkeyWithCoordinates; } catch (error) { throw new Error("Error importing or exporting key"); } } exports.createPasskey = createPasskey; async function getPassKey(challenge) { const isSupported = await isSupportedByBrowser(); if (!isSupported) { throw new Error('Your device is not supported for passkeys'); } const options = { timeout: 60000, challenge: challenge ? buffer_1.Buffer.from(challenge.slice(2), "hex") : crypto.getRandomValues(new Uint8Array(32)), rpId: window.location.hostname, userVerification: "preferred", }; const credential = await window.navigator.credentials.get({ publicKey: options, }); if (!credential) { return null; } let cred = credential; const utf8Decoder = new TextDecoder("utf-8"); const decodedClientData = utf8Decoder.decode(cred.response.clientDataJSON); const clientDataObj = JSON.parse(decodedClientData); let authenticatorData = ethers_1.ethers.utils.hexlify(new Uint8Array(cred.response.authenticatorData)); // let signature = extractSignature(new Uint8Array(cred?.response?.signature)); return { rawId: ethers_1.ethers.utils.hexlify(new Uint8Array(cred.rawId)), clientData: { type: clientDataObj.type, challenge: clientDataObj.challenge, origin: clientDataObj.origin, crossOrigin: clientDataObj.crossOrigin, }, authenticatorData, // signature, }; } exports.getPassKey = getPassKey; /** * Converts a PasskeyCredentialWithPubkeyCoordinates object to a format that can be stored in the local storage. * The rawId is required for signing and pubkey coordinates are for our convenience. * @param passkey - The passkey to be converted. * @returns The passkey in a format that can be stored in the local storage. */ function toLocalStorageFormat(passkey) { return { rawId: '0x' + buffer_1.Buffer.from(passkey.rawId).toString('hex'), pubkeyCoordinates: passkey.pubkeyCoordinates, }; } exports.toLocalStorageFormat = toLocalStorageFormat; function toLocalStorageFormatUsername(username) { return { username: username, }; } exports.toLocalStorageFormatUsername = toLocalStorageFormatUsername; /** * Checks if the provided value is in the format of a Local Storage Passkey. * @param x The value to check. * @returns A boolean indicating whether the value is in the format of a Local Storage Passkey. */ function isLocalStoragePasskey(x) { return typeof x === 'object' && x !== null && 'rawId' in x && 'pubkeyCoordinates' in x; } exports.isLocalStoragePasskey = isLocalStoragePasskey; /** * Extracts the signature into R and S values from the authenticator response. * * See: * - <https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3> * - <https://en.wikipedia.org/wiki/X.690#BER_encoding> */ function extractSignature(signature) { let sig; if (signature instanceof Uint8Array) { sig = signature.buffer; } else { sig = signature; } const check = (x) => { if (!x) { throw new Error('invalid signature encoding'); } }; // Decode the DER signature. Note that we assume that all lengths fit into 8-bit integers, // which is true for the kinds of signatures we are decoding but generally false. I.e. this // code should not be used in any serious application. const view = new DataView(sig); // check that the sequence header is valid check(view.getUint8(0) === 0x30); check(view.getUint8(1) === view.byteLength - 2); // read r and s const readInt = (offset) => { check(view.getUint8(offset) === 0x02); const len = view.getUint8(offset + 1); const start = offset + 2; const end = start + len; const n = BigInt(ethers_1.ethers.utils.hexlify(new Uint8Array(view.buffer.slice(start, end)))); check(ethers_1.BigNumber.from(n).lt(ethers_1.ethers.constants.MaxUint256)); return [n, end]; }; const [r, sOffset] = readInt(2); const [s] = readInt(sOffset); return [r, s]; } exports.extractSignature = extractSignature; /** * Compute the additional client data JSON fields. This is the fields other than `type` and * `challenge` (including `origin` and any other additional client data fields that may be * added by the authenticator). * * See <https://w3c.github.io/webauthn/#clientdatajson-serialization> */ function extractClientDataFields(response) { const clientDataJSON = new TextDecoder('utf-8').decode(response.clientDataJSON); const match = clientDataJSON.match(/^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/); if (!match) { throw new Error('challenge not found in client data JSON'); } const [, fields] = match; const result = `",${fields}}`; return { field: fields, clientDataJSON: result }; } exports.extractClientDataFields = extractClientDataFields; //# sourceMappingURL=PasskeyManager.js.map