@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
JavaScript
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
;