@ideem/zsm-client-sdk
Version:
ZSM makes 2FA easy and invisible for everyone, all the time, using advanced cryptography like MPC to establish cryptographic proof of the origin of any transaction or login attempt, while eliminating opportunities for social engineering. ZSM has no relian
126 lines (115 loc) • 12.1 kB
JavaScript
import eventCoordinator from './EventCoordinator.js';
class PasskeysPlusClient {
constructor() {
eventCoordinator.update('PasskeysPlus');
this.passkeyAPI = (navigator?.credentials??null) || null;
eventCoordinator.update('PasskeysPlus', 'READY');
}
/**
* @name pkpSupported
* @description Checks if the PasskeysPlus API is supported in the current browser.
* @returns {boolean} True if supported, false otherwise.
*/
pkpSupported = () => !!this.passkeyAPI && typeof this.passkeyAPI.create === 'function' && typeof this.passkeyAPI.get === 'function';
/**
* @name blockUnsupportedInvocation
* @description Throws an error if the PasskeysPlus API is not supported in the current browser.
* @returns {boolean} True if the invocation is supported, false otherwise.
* @throws {Error} If the PasskeysPlus API is not supported.
*/
blockUnsupportedInvocation = () => {
if(!this.pkpSupported()) throw new Error(`[PK+ PasskeysPlus] :: blockUnsupportedInvocation :: This browser and/or device does not support the PasskeysPlus API!`);
return true;
}
/**
* @name b64ToArrayBuffer
* @description Converts a base64url-encoded string to an ArrayBuffer.
* @param {string} b64 The base64url-encoded string.
* @returns {ArrayBuffer} The resulting ArrayBuffer.
* @memberof PasskeysPlus
*/
toArrayBuffer (b64) {
if (typeof b64 !== 'string' || !/^[A-Za-z0-9\-_]+$/.test(b64)) throw new Error(`[PK+ PasskeysPlus] :: toArrayBuffer :: Invalid base64url string provided: ${b64}`);
try {
const str = atob(b64.replace(/-/g, '+').replace(/_/g, '/') + ('===='.slice(0, -b64.length % 4)))
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i);
return bytes.buffer;
} catch (e) {
throw new Error(`[PK+ PasskeysPlus] :: toArrayBuffer :: Error converting base64url string to ArrayBuffer: ${e.message || e}`);
}
};
/**
* @name pkpCreatePasskeyCredential
* @description Creates a passkey credential based on the provided public key.
* @param {Object} publicKey The public key options for the passkey credential.
* @param {string} userVerification Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
* @returns {Promise<Credential>} Resolves with the created passkey credential.
* @throws {Error} The publicKey parameter was not provided or it was not an object.
* @throws {Error} The structure of the publicKey parameter is invalid.
* @throws {Error} The publicKey parameter's user ID was not an ArrayBuffer.
* @throws {Error} The publicKey parameter's pubKeyCredParams was not an Array, was empty, or contained invalid entries.
* @throws {Error} The publicKey parameter's authenticatorSelection was not an object (if provided).
* @throws {Error} The publicKey parameter's authenticatorSelection.authenticatorAttachment was missing or not a valid value.
* @throws {Error} The publicKey parameter's authenticatorSelection.requireResidentKey was missing or not a boolean.
* @throws {Error} The publicKey parameter's authenticatorSelection.userVerification was missing or not a valid value.
* @throws {Error} The publicKey parameter's timeout (if provided) was missing or not a positive number.
* @throws {Error} The publicKey parameter's excludeCredentials must be an array and cannot be empty (if provided).
* @throws {Error} The publicKey parameter's excludeCredentials (if provided) contained an entry that was not an ArrayBuffer.
* @memberof PasskeysPlus
*/
pkpCreatePasskeyCredential = async (publicKey, userVerification="preferred") => {
if(!this.blockUnsupportedInvocation()) return false;
if(publicKey?.authenticatorSelection) publicKey.authenticatorSelection.userVerification = userVerification;
if(publicKey?.extensions?.credentialProtectionPolicy) delete publicKey.extensions.credentialProtectionPolicy;
if(!publicKey || typeof publicKey !== 'object') throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid publicKey parameter provided: ${publicKey}! It must be an object. Received: ${publicKey}`);
if(!publicKey?.challenge || !publicKey?.rp || !publicKey?.rp?.id || !publicKey?.rp?.name) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid publicKey structure! It must contain challenge and rp properties with id and name. Received: ${publicKey}`);
if(!publicKey?.user || !publicKey?.user?.id || !(publicKey?.user?.id instanceof ArrayBuffer)) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid user ID in publicKey! It must be an ArrayBuffer. Received: ${publicKey?.user?.id}`);
if(!publicKey?.pubKeyCredParams || !Array.isArray(publicKey?.pubKeyCredParams)
|| publicKey?.pubKeyCredParams?.length === 0) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: pubKeyCredParams must be a non-empty array of public key credential parameters. Received: ${publicKey?.pubKeyCredParams}`);
if(!publicKey?.authenticatorSelection || typeof publicKey?.authenticatorSelection !== 'object') throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid authenticatorSelection in publicKey! It must be an object. Received: ${publicKey?.authenticatorSelection} (${typeof publicKey?.authenticatorSelection})`);
if(publicKey?.authenticatorSelection?.authenticatorAttachment
&& !['platform', 'cross-platform'].includes(publicKey?.authenticatorSelection?.authenticatorAttachment)) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid authenticatorAttachment in authenticatorSelection! It must be either "platform" or "cross-platform". Received: ${publicKey?.authenticatorSelection?.authenticatorAttachment}`);
if(publicKey?.authenticatorSelection?.requireResidentKey
&& typeof publicKey?.authenticatorSelection?.requireResidentKey !== 'boolean') throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid requireResidentKey in authenticatorSelection! It must be a boolean. Received: ${publicKey?.authenticatorSelection?.requireResidentKey} (${typeof publicKey?.authenticatorSelection?.requireResidentKey})`);
if(publicKey?.authenticatorSelection?.userVerification
&& !['required', 'preferred', 'discouraged'].includes(publicKey?.authenticatorSelection?.userVerification)) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid userVerification in authenticatorSelection! It must be one of "required", "preferred", or "discouraged". Received: ${publicKey?.authenticatorSelection?.userVerification}`);
if(!publicKey?.timeout || typeof publicKey?.timeout !== 'number' || publicKey?.timeout <= 0) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid timeout in publicKey! It must be a positive number. Received: ${publicKey?.timeout} (${typeof publicKey?.timeout})`);
if(publicKey?.excludeCredentials && (!Array.isArray(publicKey?.excludeCredentials)
|| publicKey?.excludeCredentials?.length > 0 && !publicKey?.excludeCredentials[0].id)) throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid credential ID in excludeCredentials! It must be an ArrayBuffer. Received: ${publicKey?.excludeCredentials[0]?.id}`);
publicKey.authenticatorSelection = {
...publicKey.authenticatorSelection,
residentKey: "required", // was "discouraged"
requireResidentKey: true, // was false
}
const pkpCredential = await this.passkeyAPI.create({ publicKey: {...publicKey, userVerification} });
return pkpCredential;
}
/**
* @name pkpGetPasskeyCredential
* @description Retrieves a passkey credential based on the provided public key.
* @param {Object} publicKey The public key options for the passkey credential.
* @param {string} userVerification Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
* @returns {Promise<Credential>} Resolves with the passkey credential.
* @throws {Error} The publicKey parameter was not provided or it was not an object.
* @throws {Error} The structure of the publicKey parameter is invalid.
* @throws {Error} The publicKey parameter's allowCredentials was not an array or was empty.
* @throws {Error} The publicKey parameter's allowCredentials did not contain an ArrayBuffer.
* @throws {Error} The publicKey parameter's user ID was missing or was not an ArrayBuffer.
* @throws {Error} The publicKey parameter's rp was missing or was missing name and/or id.
* @memberof PasskeysPlus
*/
pkpGetPasskeyCredential = async (publicKey, userVerification="preferred") => {
if(!this.blockUnsupportedInvocation()) return false;
if(publicKey?.authenticatorSelection) publicKey.authenticatorSelection.userVerification = userVerification;
if(!publicKey || typeof publicKey !== 'object') throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid publicKey parameter provided!`);
if(!publicKey?.challenge || !publicKey?.allowCredentials
|| !Array.isArray(publicKey?.allowCredentials)) throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid publicKey structure provided! It must contain challenge and allowCredentials properties. Received: ${publicKey}`);
if(publicKey?.allowCredentials?.length === 0) throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: allowCredentials array must not be empty! It should contain at least one credential ID. Received: ${publicKey?.allowCredentials}`);
const allIDsValid = publicKey?.allowCredentials.reduce((result, record)=>result && record?.id && (record.id instanceof ArrayBuffer || ArrayBuffer.isView(record.id)), true);
if(!allIDsValid) throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid credential ID in allowCredentials! It must be an ArrayBuffer. Received: ${publicKey?.allowCredentials[0]?.id}`);
const pkpCredential = await this.passkeyAPI.get({ publicKey: {...publicKey, userVerification} });
return pkpCredential;
}
}
export default PasskeysPlusClient;