main.js

import * as utils from "./utils.js";

import { Fido2AssertionResult, Fido2AttestationResult, Fido2Result } from "./response.js";

import { MdsCollection, MdsEntry } from "./mds.js";

// add 'none' attestation format
import { noneAttestation } from "./attestations/none.js";

// add 'packed' attestation format
import { packedAttestation } from "./attestations/packed.js";

// add 'fidoU2F' attestation format
import { fidoU2fAttestation } from "./attestations/fidoU2F.js";

// add 'androidSafetyNet' attestation format
import { androidSafetyNetAttestation } from "./attestations/androidSafetyNet.js";

// add 'tpm' attestation format
import { tpmAttestation } from "./attestations/tpm.js";

// add 'apple' attestation format
import { appleAttestation } from "./attestations/apple.js";
const {
	coerceToArrayBuffer,
	abToBuf,
	tools,
	appendBuffer,
} = utils;

const globalAttestationMap = new Map();
const globalExtensionMap = new Map();
const globalMdsCollection = new Map();

class Fido2Lib {
	/**
	 * Creates a FIDO2 server class
	 * @param {Object} opts Options for the server
	 * @param {Number} [opts.timeout=60000] The amount of time to wait, in milliseconds, before a call has timed out
	 * @param {String} [opts.rpId="localhost"] The name of the server
	 * @param {String} [opts.rpName="Anonymous Service"] The name of the server
	 * @param {String} [opts.rpIcon] A URL for the service's icon. Can be a [RFC 2397]{@link https://tools.ietf.org/html/rfc2397} data URL.
	 * @param {Number} [opts.challengeSize=64] The number of bytes to use for the challenge
	 * @param {Object} [opts.authenticatorSelection] An object describing what types of authenticators are allowed to register with the service.
	 * See [AuthenticatorSelectionCriteria]{@link https://w3.org/TR/webauthn/#authenticatorSelection} in the WebAuthn spec for details.
	 * @param {String} [opts.authenticatorAttachment] Indicates whether authenticators should be part of the OS ("platform"), or can be roaming authenticators ("cross-platform")
	 * @param {Boolean} [opts.authenticatorRequireResidentKey] Indicates whether authenticators must store the key internally (true) or if they can use a KDF to generate keys
	 * @param {String} [opts.authenticatorUserVerification] Indicates whether user verification should be performed. Options are "required", "preferred", or "discouraged".
	 * @param {String} [opts.attestation="direct"] The preferred attestation type to be used.
	 * See [AttestationConveyancePreference]{https://w3.org/TR/webauthn/#enumdef-attestationconveyancepreference} in the WebAuthn spec
	 * @param {Array<Number>} [opts.cryptoParams] A list of COSE algorithm identifiers (e.g. -7)
	 * ordered by the preference in which the authenticator should use them.
	 */
	constructor(opts) {
		/* eslint complexity: ["off"] */
		opts = opts || {};

		// set defaults
		this.config = {};

		// timeout
		this.config.timeout = (opts.timeout === undefined) ? 60000 : opts.timeout; // 1 minute
		checkOptType(this.config, "timeout", "number");
		if (!(this.config.timeout >>> 0 === parseFloat(this.config.timeout))) {
			throw new RangeError("timeout should be zero or positive integer");
		}

		// challengeSize
		this.config.challengeSize = opts.challengeSize || 64;
		checkOptType(this.config, "challengeSize", "number");
		if (this.config.challengeSize < 32) {
			throw new RangeError(
				"challenge size too small, must be 32 or greater",
			);
		}

		// rpId
		this.config.rpId = opts.rpId;
		checkOptType(this.config, "rpId", "string");

		// rpName
		this.config.rpName = opts.rpName || "Anonymous Service";
		checkOptType(this.config, "rpName", "string");

		// rpIcon
		this.config.rpIcon = opts.rpIcon;
		checkOptType(this.config, "rpIcon", "string");

		// authenticatorRequireResidentKey
		this.config.authenticatorRequireResidentKey = opts.authenticatorRequireResidentKey;
		checkOptType(this.config, "authenticatorRequireResidentKey", "boolean");

		// authenticatorAttachment
		this.config.authenticatorAttachment = opts.authenticatorAttachment;
		if (
			this.config.authenticatorAttachment !== undefined &&
			(this.config.authenticatorAttachment !== "platform" &&
				this.config.authenticatorAttachment !== "cross-platform")
		) {
			throw new TypeError(
				"expected authenticatorAttachment to be 'platform', or 'cross-platform', got: " +
					this.config.authenticatorAttachment,
			);
		}

		// authenticatorUserVerification
		this.config.authenticatorUserVerification = opts.authenticatorUserVerification;
		if (
			this.config.authenticatorUserVerification !== undefined &&
			(this.config.authenticatorUserVerification !== "required" &&
				this.config.authenticatorUserVerification !== "preferred" &&
				this.config.authenticatorUserVerification !== "discouraged")
		) {
			throw new TypeError(
				"expected authenticatorUserVerification to be 'required', 'preferred', or 'discouraged', got: " +
					this.config.authenticatorUserVerification,
			);
		}

		// attestation
		this.config.attestation = opts.attestation || "direct";
		if (
			this.config.attestation !== "direct" &&
			this.config.attestation !== "indirect" &&
			this.config.attestation !== "none"
		) {
			throw new TypeError(
				"expected attestation to be 'direct', 'indirect', or 'none', got: " +
					this.config.attestation,
			);
		}

		// cryptoParams
		this.config.cryptoParams = opts.cryptoParams || [-7, -257];
		checkOptType(this.config, "cryptoParams", Array);
		if (this.config.cryptoParams.length < 1) {
			throw new TypeError("cryptoParams must have at least one element");
		}
		this.config.cryptoParams.forEach((param) => {
			checkOptType({ cryptoParam: param }, "cryptoParam", "number");
		});

		this.attestationMap = globalAttestationMap;
		this.extSet = new Set(); // enabled extensions (all disabled by default)
		this.extOptMap = new Map(); // default options for extensions

		// TODO: convert icon file to data-URL icon
		// TODO: userVerification
	}

	/**
	 * Creates a new {@link MdsCollection}
	 * @param {String} collectionName The name of the collection to create.
	 * Used to identify the source of a {@link MdsEntry} when {@link Fido2Lib#findMdsEntry}
	 * finds multiple matching entries from different sources (e.g. FIDO MDS 1 & FIDO MDS 2)
	 * @return {MdsCollection} The MdsCollection that was created
	 * @see  MdsCollection
	 */
	static createMdsCollection(collectionName) {
		return new MdsCollection(collectionName);
	}

	/**
	 * Adds a new {@link MdsCollection} to the global MDS collection list that will be used for {@link findMdsEntry}
	 * @param {MdsCollection} mdsCollection The MDS collection that will be used
	 * @see  MdsCollection
	 */
	static async addMdsCollection(mdsCollection) {
		if (!(mdsCollection instanceof MdsCollection)) {
			throw new Error(
				"expected 'mdsCollection' to be instance of MdsCollection, got: " +
					mdsCollection,
			);
		}
		await mdsCollection.validate();
		globalMdsCollection.set(mdsCollection.name, mdsCollection);
	}

	/**
	 * Removes all entries from the global MDS collections list. Mostly used for testing.
	 */
	static clearMdsCollections() {
		globalMdsCollection.clear();
	}

	/**
	 * Returns {@link MdsEntry} objects that match the requested id. The
	 * lookup is done by calling {@link MdsCollection#findEntry} on the current global
	 * MDS collection. If no global MDS collection has been specified using
	 * {@link setMdsCollection}, an `Error` will be thrown.
	 * @param  {String|ArrayBuffer} id The authenticator id to look up metadata for
	 * @return {Array.<MdsEntry>}    Returns an Array of {@link MdsEntry} for the specified id.
	 * If no entry was found, the Array will be empty.
	 * @see  MdsCollection
	 */
	static findMdsEntry(id) {
		if (globalMdsCollection.size < 1) {
			throw new Error(
				"must set MDS collection before attempting to find an MDS entry",
			);
		}

		const ret = [];
		for (const collection of globalMdsCollection.values()) {
			const entry = collection.findEntry(id);
			if (entry) ret.push(entry);
		}

		return ret;
	}

	/**
	 * Adds a new global extension that will be available to all instantiations of
	 * {@link Fido2Lib}. Note that the extension must still be enabled by calling
	 * {@link enableExtension} for each instantiation of a Fido2Lib.
	 * @param {String} extName     The name of the extension to add. (e.g. - "appid")
	 * @param {Function} optionGeneratorFn Extensions are included in
	 * @param {Function} resultParserFn    [description]
	 * @param {Function} resultValidatorFn [description]
	 */
	static addExtension(
		extName,
		optionGeneratorFn,
		resultParserFn,
		resultValidatorFn,
	) {
		if (typeof extName !== "string") {
			throw new Error("expected 'extName' to be String, got: " + extName);
		}

		if (globalExtensionMap.has(extName)) {
			throw new Error(
				`the extension '${extName}' has already been added`,
			);
		}

		if (typeof optionGeneratorFn !== "function") {
			throw new Error(
				"expected 'optionGeneratorFn' to be a Function, got: " +
					optionGeneratorFn,
			);
		}

		if (typeof resultParserFn !== "function") {
			throw new Error(
				"expected 'resultParserFn' to be a Function, got: " +
					resultParserFn,
			);
		}

		if (typeof resultValidatorFn !== "function") {
			throw new Error(
				"expected 'resultValidatorFn' to be a Function, got: " +
					resultValidatorFn,
			);
		}

		globalExtensionMap.set(extName, {
			optionGeneratorFn,
			resultParserFn,
			resultValidatorFn,
		});
	}

	/**
	 * Removes all extensions from the global extension registry. Mostly used for testing.
	 */
	static deleteAllExtensions() {
		globalExtensionMap.clear();
	}

	/**
	 * Generates the options to send to the client for the specified extension
	 * @private
	 * @param  {String} extName The name of the extension to generate options for. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
	 * @param  {String} type    The type of options that are being generated. Valid options are "attestation" or "assertion".
	 * @param  {Any} [options] Optional parameters to pass to the generator function
	 * @return {Any}         The extension value that will be sent to the client. If `undefined`, this extension won't be included in the
	 * options sent to the client.
	 */
	generateExtensionOptions(extName, type, options) {
		if (typeof extName !== "string") {
			throw new Error("expected 'extName' to be String, got: " + extName);
		}

		if (type !== "attestation" && type !== "assertion") {
			throw new Error(
				"expected 'type' to be 'attestation' or 'assertion', got: " +
					type,
			);
		}

		const ext = globalExtensionMap.get(extName);
		if (
			typeof ext !== "object" ||
			typeof ext.optionGeneratorFn !== "function"
		) {
			throw new Error(`valid extension for '${extName}' not found`);
		}
		const ret = ext.optionGeneratorFn(extName, type, options);

		return ret;
	}

	static parseExtensionResult(extName, clientThing, authnrThing) {
		if (typeof extName !== "string") {
			throw new Error("expected 'extName' to be String, got: " + extName);
		}

		const ext = globalExtensionMap.get(extName);
		if (
			typeof ext !== "object" ||
			typeof ext.parseFn !== "function"
		) {
			throw new Error(`valid extension for '${extName}' not found`);
		}
		const ret = ext.parseFn(extName, clientThing, authnrThing);

		return ret;
	}

	static validateExtensionResult(extName) {
		const ext = globalExtensionMap.get(extName);
		if (
			typeof ext !== "object" ||
			typeof ext.validateFn !== "function"
		) {
			throw new Error(`valid extension for '${extName}' not found`);
		}
		const ret = ext.validateFn.call(this);

		return ret;
	}

	/**
	 * Enables the specified extension.
	 * @param  {String} extName The name of the extension to enable. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
	 */
	enableExtension(extName) {
		if (typeof extName !== "string") {
			throw new Error("expected 'extName' to be String, got: " + extName);
		}

		if (!globalExtensionMap.has(extName)) {
			throw new Error(`valid extension for '${extName}' not found`);
		}

		this.extSet.add(extName);
	}

	/**
	 * Disables the specified extension.
	 * @param  {String} extName The name of the extension to enable. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
	 */
	disableExtension(extName) {
		if (typeof extName !== "string") {
			throw new Error("expected 'extName' to be String, got: " + extName);
		}

		if (!globalExtensionMap.has(extName)) {
			throw new Error(`valid extension for '${extName}' not found`);
		}

		this.extSet.delete(extName);
	}

	/**
	 * Specifies the options to be used for the extension
	 * @param  {String} extName The name of the extension to set the options for (e.g. - "appid". Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
	 * @param {Any} options The parameter that will be passed to the option generator function (e.g. - "https://webauthn.org")
	 */
	setExtensionOptions(extName, options) {
		if (typeof extName !== "string") {
			throw new Error("expected 'extName' to be String, got: " + extName);
		}

		if (!globalExtensionMap.has(extName)) {
			throw new Error(`valid extension for '${extName}' not found`);
		}

		this.extOptMap.set(extName, options);
	}

	/**
	 * Validates an attestation response. Will be called within the context (`this`) of a {@link Fido2AttestationResult}
	 * @private
	 */
	static async validateAttestation() {
		const fmt = this.authnrData.get("fmt");

		// validate input
		if (typeof fmt !== "string") {
			throw new TypeError(
				"expected 'fmt' to be string, got: " + typeof fmt,
			);
		}

		// get from attestationMap
		const fmtObj = globalAttestationMap.get(fmt);
		if (
			typeof fmtObj !== "object" ||
			typeof fmtObj.parseFn !== "function" ||
			typeof fmtObj.validateFn !== "function"
		) {
			throw new Error(`no support for attestation format: ${fmt}`);
		}

		// call fn
		const ret = await fmtObj.validateFn.call(this);

		// validate return
		if (ret !== true) {
			throw new Error(`${fmt} validateFn did not return 'true'`);
		}

		// return result
		return ret;
	}

	/**
	 * Adds a new attestation format that will automatically be recognized and parsed
	 * for any future {@link Fido2CreateRequest} messages
	 * @param {String} fmt The name of the attestation format, as it appears in the
	 * ARIN registry and / or as it will appear in the {@link Fido2CreateRequest}
	 * message that is received
	 * @param {Function} parseFn The function that will be called to parse the
	 * attestation format. It will receive the `attStmt` as a parameter and will be
	 * called from the context (`this`) of the `Fido2CreateRequest`
	 * @param {Function} validateFn The function that will be called to validate the
	 * attestation format. It will receive no arguments, as all the necessary
	 * information for validating the attestation statement will be contained in the
	 * calling context (`this`).
	 */
	static addAttestationFormat(fmt, parseFn, validateFn) {
		// validate input
		if (typeof fmt !== "string") {
			throw new TypeError(
				"expected 'fmt' to be string, got: " + typeof fmt,
			);
		}

		if (typeof parseFn !== "function") {
			throw new TypeError(
				"expected 'parseFn' to be string, got: " + typeof parseFn,
			);
		}

		if (typeof validateFn !== "function") {
			throw new TypeError(
				"expected 'validateFn' to be string, got: " + typeof validateFn,
			);
		}

		if (globalAttestationMap.has(fmt)) {
			throw new Error(`can't add format: '${fmt}' already exists`);
		}

		// add to attestationMap
		globalAttestationMap.set(fmt, {
			parseFn,
			validateFn,
		});

		return true;
	}

	/**
	 * Deletes all currently registered attestation formats.
	 */
	static deleteAllAttestationFormats() {
		globalAttestationMap.clear();
	}

	/**
	 * Parses an attestation statememnt of the format specified
	 * @private
	 * @param {String} fmt The name of the format to be parsed, as specified in the
	 * ARIN registry of attestation formats.
	 * @param {Object} attStmt The attestation object to be parsed.
	 * @return {Map} A Map of all the attestation fields that were parsed.
	 * At this point the fields have not yet been verified.
	 * @throws {Error} when a field cannot be parsed or verified.
	 * @throws {TypeError} when supplied parameters `fmt` or `attStmt` are of the
	 * wrong type
	 */
	static parseAttestation(fmt, attStmt) {
		// validate input
		if (typeof fmt !== "string") {
			throw new TypeError(
				"expected 'fmt' to be string, got: " + typeof fmt,
			);
		}

		if (typeof attStmt !== "object") {
			throw new TypeError(
				"expected 'attStmt' to be object, got: " + typeof attStmt,
			);
		}

		// get from attestationMap
		const fmtObj = globalAttestationMap.get(fmt);
		if (
			typeof fmtObj !== "object" ||
			typeof fmtObj.parseFn !== "function" ||
			typeof fmtObj.validateFn !== "function"
		) {
			throw new Error(`no support for attestation format: ${fmt}`);
		}

		// call fn
		const ret = fmtObj.parseFn.call(this, attStmt);

		// validate return
		if (!(ret instanceof Map)) {
			throw new Error(`${fmt} parseFn did not return a Map`);
		}

		// return result
		return new Map([
			["fmt", fmt],
			...ret,
		]);
	}

	/**
	 * Parses and validates an attestation response from the client
	 * @param {Object} res The assertion result that was generated by the client.
	 * See {@link https://w3.org/TR/webauthn/#authenticatorattestationresponse AuthenticatorAttestationResponse} in the WebAuthn spec.
	 * @param {String} [res.id] The base64url encoded id returned by the client
	 * @param {String} [res.rawId] The base64url encoded rawId returned by the client. If `res.rawId` is missing, `res.id` will be used instead. If both are missing an error will be thrown.
	 * @param {String} res.response.clientDataJSON The base64url encoded clientDataJSON returned by the client
	 * @param {String} res.response.authenticatorData The base64url encoded authenticatorData returned by the client
	 * @param {Object} expected The expected parameters for the assertion response.
	 * If these parameters don't match the recieved values, validation will fail and an error will be thrown.
	 * @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions}
	 * @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org"
	 * @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either".
	 * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.).
	 * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button).
	 * If "either", then either "first" or "second" is acceptable
	 * @return {Promise<Fido2AttestationResult>} Returns a Promise that resolves to a {@link Fido2AttestationResult}
	 * @throws {Error} If parsing or validation fails
	 */
	async attestationResult(res, expected) {
		expected.flags = factorToFlags(expected.factor, ["AT"]);
		delete expected.factor;
		return await Fido2AttestationResult.create(res, expected);
	}

	/**
	 * Parses and validates an assertion response from the client
	 * @param {Object} res The assertion result that was generated by the client.
	 * See {@link https://w3.org/TR/webauthn/#authenticatorassertionresponse AuthenticatorAssertionResponse} in the WebAuthn spec.
	 * @param {String} [res.id] The base64url encoded id returned by the client
	 * @param {String} [res.rawId] The base64url encoded rawId returned by the client. If `res.rawId` is missing, `res.id` will be used instead. If both are missing an error will be thrown.
	 * @param {String} res.response.clientDataJSON The base64url encoded clientDataJSON returned by the client
	 * @param {String} res.response.attestationObject The base64url encoded authenticatorData returned by the client
	 * @param {String} res.response.signature The base64url encoded signature returned by the client
	 * @param {String|null} [res.response.userHandle] The base64url encoded userHandle returned by the client. May be null or an empty string.
	 * @param {Object} expected The expected parameters for the assertion response.
	 * If these parameters don't match the recieved values, validation will fail and an error will be thrown.
	 * @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions}
	 * @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org"
	 * @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either".
	 * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.).
	 * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button).
	 * If "either", then either "first" or "second" is acceptable
	 * @param {String} expected.publicKey A PEM encoded public key that will be used to validate the assertion response signature.
	 * This is the public key that was returned for this user during [attestationResult]{@link Fido2Lib#attestationResult}
	 * @param {Number} expected.prevCounter The previous value of the signature counter for this authenticator.
	 * @param {String|null} expected.userHandle The expected userHandle, which was the user.id during registration
	 * @return {Promise<Fido2AssertionResult>} Returns a Promise that resolves to a {@link Fido2AssertionResult}
	 * @throws {Error} If parsing or validation fails
	 */
	// deno-lint-ignore require-await
	async assertionResult(res, expected) {
		expected.flags = factorToFlags(expected.factor, []);
		delete expected.factor;
		return Fido2AssertionResult.create(res, expected);
	}

	/**
	 * Gets a challenge and any other parameters for the `navigator.credentials.create()` call
	 * The `challenge` property is an `ArrayBuffer` and will need to be encoded to be transmitted to the client.
	 * @param {Object} [opts] An object containing various options for the option creation
	 * @param {Object} [opts.extensionOptions] An object that contains the extensions to enable, and the options to use for each of them.
	 * The keys of this object are the names of the extensions (e.g. - "appid"), and the value of each key is the option that will
	 * be passed to that extension when it is generating the value to send to the client. This object overrides the extensions that
	 * have been set with {@link enableExtension} and the options that have been set with {@link setExtensionOptions}. If an extension
	 * was enabled with {@link enableExtension} but it isn't included in this object, the extension won't be sent to the client. Likewise,
	 * if an extension was disabled with {@link disableExtension} but it is included in this object, it will be sent to the client.
	 * @param {String} [extraData] Extra data to be signed by the authenticator during attestation. The challenge will be a hash:
	 * SHA256(rawChallenge + extraData) and the `rawChallenge` will be returned as part of PublicKeyCredentialCreationOptions.
	 * @returns {Promise<PublicKeyCredentialCreationOptions>} The options for creating calling `navigator.credentials.create()`
	 */
	async attestationOptions(opts) {
		opts = opts || {};

		// The object being returned is described here:
		// https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions
		let challenge = tools.randomValues(this.config.challengeSize);
		challenge = coerceToArrayBuffer(challenge, "challenge");
		const pubKeyCredParams = [];
		this.config.cryptoParams.forEach((coseId) => {
			pubKeyCredParams.push({
				type: "public-key",
				alg: coseId,
			});
		});

		// mix extraData into challenge
		let rawChallenge;
		if (opts.extraData) {
			rawChallenge = challenge;
			const extraData = coerceToArrayBuffer(opts.extraData, "extraData");
			const hash = await tools.hashDigest(
				appendBuffer(challenge, extraData),
			);
			challenge = new Uint8Array(hash).buffer;
		}

		const options = {
			rp: {},
			user: {},
		};

		const extensions = createExtensions.call(
			this,
			"attestation",
			opts.extensionOptions,
		);

		/**
		 * @typedef {Object} PublicKeyCredentialCreationOptions
		 * @description This object is returned by {@link attestationOptions} and is basially the same as
		 * the [PublicKeyCredentialCreationOptions]{@link https://w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions}
		 * object that is required to be passed to `navigator.credentials.create()`. With the exception of the `challenge` property,
		 * all other properties are optional and only set if they were specified in the configuration paramater
		 * that was passed to the constructor.
		 * @property {Object} rp Relying party information (a.k.a. - server / service information)
		 * @property {String} [rp.name] Relying party name (e.g. - "ACME"). This is only set if `rpName` was specified during the `new` call.
		 * @property {String} [rp.id] Relying party ID, a domain name (e.g. - "example.com"). This is only set if `rpId` was specified during the `new` call.
		 * @property {Object} user User information. This will be an empty object
		 * @property {ArrayBuffer} challenge An ArrayBuffer filled with random bytes. This will be verified in {@link attestationResult}
		 * @property {Array} [pubKeyCredParams] A list of PublicKeyCredentialParameters objects, based on the `cryptoParams` that was passed to the constructor.
		 * @property {Number} [timeout] The amount of time that the call should take before returning an error
		 * @property {String} [attestation] Whether the client should request attestation from the authenticator or not
		 * @property {Object} [authenticatorSelection] A object describing which authenticators are preferred for registration
		 * @property {String} [authenticatorSelection.attachment] What type of attachement is acceptable for new authenticators.
		 * Allowed values are "platform", meaning that the authenticator is embedded in the operating system, or
		 * "cross-platform", meaning that the authenticator is removeable (e.g. USB, NFC, or BLE).
		 * @property {Boolean} [authenticatorSelection.requireResidentKey] Indicates whether authenticators must store the keys internally, or if they can
		 * store them externally (using a KDF or key wrapping)
		 * @property {String} [authenticatorSelection.userVerification] Indicates whether user verification is required for authenticators. User verification
		 * means that an authenticator will validate a use through their biometrics (e.g. fingerprint) or knowledge (e.g. PIN). Allowed
		 * values for `userVerification` are "required", meaning that registration will fail if no authenticator provides user verification;
		 * "preferred", meaning that if multiple authenticators are available, the one(s) that provide user verification should be used; or
		 * "discouraged", which means that authenticators that don't provide user verification are preferred.
		 * @property {ArrayBuffer} [rawChallenge] If `extraData` was passed to {@link attestationOptions}, this
		 * will be the original challenge used, and `challenge` will be a hash:
		 * SHA256(rawChallenge + extraData)
		 * @property {Object} [extensions] The values of any enabled extensions.
		 */
		setOpt(options.rp, "name", this.config.rpName);
		setOpt(options.rp, "id", this.config.rpId);
		setOpt(options.rp, "icon", this.config.rpIcon);
		setOpt(options, "challenge", challenge);
		setOpt(options, "pubKeyCredParams", pubKeyCredParams);
		setOpt(options, "timeout", this.config.timeout);
		setOpt(options, "attestation", this.config.attestation);
		if (
			this.config.authenticatorAttachment !== undefined ||
			this.config.authenticatorRequireResidentKey !== undefined ||
			this.config.authenticatorUserVerification !== undefined
		) {
			options.authenticatorSelection = {};
			setOpt(
				options.authenticatorSelection,
				"authenticatorAttachment",
				this.config.authenticatorAttachment,
			);
			setOpt(
				options.authenticatorSelection,
				"requireResidentKey",
				this.config.authenticatorRequireResidentKey,
			);
			setOpt(
				options.authenticatorSelection,
				"userVerification",
				this.config.authenticatorUserVerification,
			);
		}
		setOpt(options, "rawChallenge", rawChallenge);

		if (Object.keys(extensions).length > 0) {
			options.extensions = extensions;
		}

		return options;
	}

	/**
	 * Creates an assertion challenge and any other parameters for the `navigator.credentials.get()` call.
	 * The `challenge` property is an `ArrayBuffer` and will need to be encoded to be transmitted to the client.
	 * @param {Object} [opts] An object containing various options for the option creation
	 * @param {Object} [opts.extensionOptions] An object that contains the extensions to enable, and the options to use for each of them.
	 * The keys of this object are the names of the extensions (e.g. - "appid"), and the value of each key is the option that will
	 * be passed to that extension when it is generating the value to send to the client. This object overrides the extensions that
	 * have been set with {@link enableExtension} and the options that have been set with {@link setExtensionOptions}. If an extension
	 * was enabled with {@link enableExtension} but it isn't included in this object, the extension won't be sent to the client. Likewise,
	 * if an extension was disabled with {@link disableExtension} but it is included in this object, it will be sent to the client.
	 * @param {String} [extraData] Extra data to be signed by the authenticator during attestation. The challenge will be a hash:
	 * SHA256(rawChallenge + extraData) and the `rawChallenge` will be returned as part of PublicKeyCredentialCreationOptions.
	 * @returns {Promise<PublicKeyCredentialRequestOptions>} The options to be passed to `navigator.credentials.get()`
	 */
	async assertionOptions(opts) {
		opts = opts || {};

		// https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions
		let challenge = tools.randomValues(this.config.challengeSize);
		challenge = coerceToArrayBuffer(challenge, "challenge");
		const options = {};

		// mix extraData into challenge
		let rawChallenge;
		if (opts.extraData) {
			rawChallenge = challenge;
			const extraData = coerceToArrayBuffer(opts.extraData, "extraData");
			challenge = abToBuf(
				await tools.hashDigest(appendBuffer(challenge, extraData)),
			);
		}

		const extensions = createExtensions.call(
			this,
			"assertion",
			opts.extensionOptions,
		);

		/**
		 * @typedef {Object} PublicKeyCredentialRequestOptions
		 * @description This object is returned by {@link assertionOptions} and is basially the same as
		 * the [PublicKeyCredentialRequestOptions]{@link https://w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions}
		 * object that is required to be passed to `navigator.credentials.get()`. With the exception of the `challenge` property,
		 * all other properties are optional and only set if they were specified in the configuration paramater
		 * that was passed to the constructor.
		 * @property {ArrayBuffer} challenge An ArrayBuffer filled with random bytes. This will be verified in {@link attestationResult}
		 * @property {Number} [timeout] The amount of time that the call should take before returning an error
		 * @property {String} [rpId] Relying party ID, a domain name (e.g. - "example.com"). This is only set if `rpId` was specified during the `new` call.
		 * @property {String} [attestation] Whether the client should request attestation from the authenticator or not
		 * @property {String} [userVerification] Indicates whether user verification is required for authenticators. User verification
		 * means that an authenticator will validate a use through their biometrics (e.g. fingerprint) or knowledge (e.g. PIN). Allowed
		 * values for `userVerification` are "required", meaning that authentication will fail if no authenticator provides user verification;
		 * "preferred", meaning that if multiple authenticators are available, the one(s) that provide user verification should be used; or
		 * "discouraged", which means that authenticators that don't provide user verification are preferred.
		 * @property {ArrayBuffer} [rawChallenge] If `extraData` was passed to {@link attestationOptions}, this
		 * will be the original challenge used, and `challenge` will be a hash:
		 * SHA256(rawChallenge + extraData)
		 * @property {Object} [extensions] The values of any enabled extensions.
		 */
		setOpt(options, "challenge", challenge);
		setOpt(options, "timeout", this.config.timeout);
		setOpt(options, "rpId", this.config.rpId);
		setOpt(
			options,
			"userVerification",
			this.config.authenticatorUserVerification,
		);

		setOpt(options, "rawChallenge", rawChallenge);

		if (Object.keys(extensions).length > 0) {
			options.extensions = extensions;
		}

		return options;
	}
}

function checkOptType(opts, prop, type) {
	if (typeof opts !== "object") return;

	// undefined
	if (opts[prop] === undefined) return;

	// native type
	if (typeof type === "string") {
		// deno-lint-ignore valid-typeof
		if (typeof opts[prop] !== type) {
			throw new TypeError(
				`expected ${prop} to be ${type}, got: ${opts[prop]}`,
			);
		}
	}

	// class type
	if (typeof type === "function") {
		if (!(opts[prop] instanceof type)) {
			throw new TypeError(
				`expected ${prop} to be ${type.name}, got: ${opts[prop]}`,
			);
		}
	}
}

function setOpt(obj, prop, val) {
	if (val !== undefined) {
		obj[prop] = val;
	}
}

function factorToFlags(expectedFactor, flags) {
	// var flags = ["AT"];
	flags = flags || [];

	switch (expectedFactor) {
		case "first":
			flags.push("UP");
			flags.push("UV");
			break;
		case "second":
			flags.push("UP");
			break;
		case "either":
			flags.push("UP-or-UV");
			break;
		default:
			throw new TypeError(
				"expectedFactor should be 'first', 'second' or 'either'",
			);
	}

	return flags;
}

function createExtensions(type, extObj) {
	/* eslint-disable no-invalid-this */
	const extensions = {};

	// default extensions
	let enabledExtensions = this.extSet;
	let extensionsOptions = this.extOptMap;

	// passed in extensions
	if (typeof extObj === "object") {
		enabledExtensions = new Set(Object.keys(extObj));
		extensionsOptions = new Map();
		for (const key of Object.keys(extObj)) {
			extensionsOptions.set(key, extObj[key]);
		}
	}

	// generate extension values
	for (const extension of enabledExtensions) {
		const extVal = this.generateExtensionOptions(
			extension,
			type,
			extensionsOptions.get(extension),
		);
		if (extVal !== undefined) extensions[extension] = extVal;
	}

	return extensions;
}
Fido2Lib.addAttestationFormat(
	noneAttestation.name,
	noneAttestation.parseFn,
	noneAttestation.validateFn,
);
Fido2Lib.addAttestationFormat(
	packedAttestation.name,
	packedAttestation.parseFn,
	packedAttestation.validateFn,
);
Fido2Lib.addAttestationFormat(
	fidoU2fAttestation.name,
	fidoU2fAttestation.parseFn,
	fidoU2fAttestation.validateFn,
);
Fido2Lib.addAttestationFormat(
	androidSafetyNetAttestation.name,
	androidSafetyNetAttestation.parseFn,
	androidSafetyNetAttestation.validateFn,
);
Fido2Lib.addAttestationFormat(
	tpmAttestation.name,
	tpmAttestation.parseFn,
	tpmAttestation.validateFn,
);
Fido2Lib.addAttestationFormat(
	appleAttestation.name,
	appleAttestation.parseFn,
	appleAttestation.validateFn
);
export { Fido2Lib };

// Export all helpers

export {
	arrayBufferEquals,
	abToBuf,
	abToHex,
	appendBuffer,
	coerceToArrayBuffer,
	coerceToBase64,
	coerceToBase64Url,
	isBase64Url,
	isPem,
	jsObjectToB64,
	pemToBase64,
	str2ab,
	tools
} from "./utils.js";

export {
	parseAttestationObject,
	parseAuthenticatorData,
	parseAuthnrAssertionResponse,
	parseAuthnrAttestationResponse,
	parseClientResponse,
	parseExpectations
} from "./parser.js";

export { Certificate, CertManager, CRL, helpers } from "./certUtils.js";
export { PublicKey, coseAlgToHashStr, coseAlgToStr } from "./keyUtils.js";

// Export response
export { Fido2AssertionResult, Fido2AttestationResult, Fido2Result };

// Export validator
export { attach } from "./validator.js";

// Export mds
export { MdsCollection, MdsEntry };

// Export attestations
export { androidSafetyNetAttestation, fidoU2fAttestation, noneAttestation, packedAttestation, tpmAttestation, appleAttestation };