+ /**
+ * Make a new key, of the assigned encoding.
+ * @param {String=} encoding
+ * @param {String=} algorithm
+ * @returns {String|Buffer}
+ */
+ static async createKey(algorithm = 'sha1', encoding = 'hex') {
+ const key = await randomBytesAsync(this._algorithmKeyLength(algorithm));
+ switch (encoding) {
+ case 'buffer':
+ return key;
+ case 'base32':
+ return this._b32Encode(key);
+ default:
+ return key.toString(encoding);
+ }
+ }
+
+ /**
+ * @typedef {Object} OtpAuthData
+ * @property {String} secret
+ * @property {String} svg
+ * @property {String} uri
+ */
+ /**
+ * Given a key, return data suitable for an authenticator client to ingest
+ * it, as a qrcode SVG, the otpauth uri encoded in the qrcode SVG, and the
+ * secret key encoded as base32.
+ * @param {Object} options
+ * @param {String} options.accountname
+ * @param {BigInt=} options.counter
+ * @param {String=} options.issuer
+ * @param {String=} options.scheme
+ * @param {String=} options.type
+ * @param {String=} options.algorithm
+ * @param {String=} options.digits
+ * @param {Number=} options.svgPadding
+ * @param {Number=} options.svgWidth
+ * @param {Number=} options.svgHeight
+ * @param {String=} options.svgFg
+ * @param {String=} options.svgBg
+ * @param {String=} options.svgEcl
+ * @param {Boolean=} options.join
+ * @param {Boolean=} options.xmlDeclaration
+ * @param {String=} options.container
+ * @param {String|Buffer} key
+ * @param {String=} keyEncoding
+ * @returns {OtpAuthData}
+ */
+ static createKeySVG(options, key, keyEncoding = 'hex') {
+ // Normalize key to base32 ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 string (rfc4648)
+ let keyBuffer, keyB32;
+ switch (keyEncoding) {
+ case 'base32':
+ keyB32 = key;
+ break;
+ case 'buffer':
+ keyBuffer = key;
+ break;
+ default:
+ keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, keyEncoding);
+ }
+ if (keyBuffer) {
+ const encoder = new B32.Encoder();
+ keyB32 = encoder.write(keyBuffer).finalize();
+ }
+ const uri = this._qrURI({
+ ...options,
+ secret: keyB32,
+ });
+
+ const qrcode = new QRCode({
+ content: uri,
+ padding: options.svgPadding ?? 4,
+ width: options.svgWidth ?? 300,
+ height: options.svgHeight ?? 300,
+ color: options.svgFg || '#000000',
+ background: options.svgBg || '#ffffff',
+ ecl: options.svgEcl || 'M',
+ join: options.join ?? true,
+ xmlDeclaration: options.xmlDeclaration ?? false,
+ container: options.container || 'svg-viewbox',
+ });
+
+ return {
+ secret: keyB32,
+ svg: qrcode.svg(),
+ uri,
+ };
+ }
+
+ /**
+ * Render parameters as an otpauth URI.
+ * @param {Object} options
+ * @param {String} options.accountname
+ * @param {String} options.secret base32
+ * @param {BigInt=} options.counter
+ * @param {String=} options.issuer
+ * @param {String=} options.scheme
+ * @param {String=} options.type
+ * @param {String=} options.algorithm
+ * @param {String=} options.digits
+ */
+ static _qrURI(options) {
+ const {
+ accountname,
+ secret,
+ counter,
+ issuer,
+ scheme,
+ type,
+ algorithm,
+ digits,
+ } = { ...this._qrURIDefaultOptions, ...options };
+ if (!accountname) {
+ throw new RangeError('missing accountname');
+ }
+ if (!secret) {
+ throw new RangeError('missing secret');
+ }
+ if (type === 'hotp' && counter === undefined) {
+ throw new RangeError('htop must include counter');
+ }
+ if (digits && ![6, 8].includes(digits)) {
+ throw new RangeError('digits out of range');
+ }
+ if (algorithm && !(algorithm.toLowerCase() in this._algorithmKeyLengths)) {
+ throw new RangeError('unsupported algorithm');
+ }
+ const label = `${issuer}${issuer ? ':' : ''}${accountname}`;
+ const url = new URL(`${scheme}://${type}/`);
+ url.pathname = label;
+ Object.entries({
+ secret,
+ ...(issuer && { issuer }),
+ ...(type === 'hotp' && counter !== undefined && { counter: counter.toString }),
+ ...(algorithm && { algorithm: algorithm.toUpperCase() }),
+ ...(digits && { digits }),
+ }).forEach(([k, v]) => {
+ url.searchParams.set(k, v);
+ });
+
+ return url.href;
+ }
+
+ static get _qrURIDefaultOptions() {
+ return {
+ scheme: 'otpauth',
+ type: this._type,
+ };
+ }
+