otpauth
Version:
One Time Password (HOTP/TOTP) library for Node.js and browsers
171 lines (148 loc) • 4.41 kB
JavaScript
import { Utils } from './utils';
import { Secret } from './secret';
// eslint-disable-next-line import/no-cycle
import { HOTP, TOTP } from './otp';
/**
* Valid key URI parameters.
* @private
* @type {Array}
*/
const OTPURI_PARAMS = ['issuer', 'secret', 'algorithm', 'digits', 'counter', 'period'];
/**
* Key URI regex.
* otpauth://TYPE/[ISSUER:]LABEL?PARAMETERS
* @private
* @type {RegExp}
*/
const OTPURI_REGEX = new RegExp(`^otpauth:\\/\\/([ht]otp)\\/(.+)\\?((?:&?(?:${OTPURI_PARAMS.join('|')})=[^&]+)+)$`, 'i');
/**
* RFC 4648 base32 alphabet with pad.
* @private
* @type {string}
*/
const SECRET_REGEX = /^[2-7A-Z]+=*$/i;
/**
* Regex for supported algorithms.
* @private
* @type {RegExp}
*/
const ALGORITHM_REGEX = /^SHA(?:1|256|512)$/i;
/**
* Integer regex.
* @private
* @type {RegExp}
*/
const INTEGER_REGEX = /^[+-]?\d+$/;
/**
* Positive integer regex.
* @private
* @type {RegExp}
*/
const POSITIVE_INTEGER_REGEX = /^\+?[1-9]\d*$/;
/**
* HOTP/TOTP object/string conversion
* (https://github.com/google/google-authenticator/wiki/Key-Uri-Format).
*/
export class URI {
/**
* Parses a Google Authenticator key URI and returns an HOTP/TOTP object.
* @param {string} uri Google Authenticator Key URI.
* @returns {HOTP|TOTP} HOTP/TOTP object.
*/
static parse(uri) {
let uriGroups;
try {
uriGroups = uri.match(OTPURI_REGEX);
} catch (error) { /* Handled below */ }
if (!Array.isArray(uriGroups)) {
throw new URIError('Invalid URI format');
}
// Extract URI groups.
const uriType = uriGroups[1].toLowerCase();
const uriLabel = uriGroups[2].split(/:(.+)/, 2).map(decodeURIComponent);
const uriParams = uriGroups[3].split('&').reduce((acc, cur) => {
const pairArr = cur.split(/=(.+)/, 2).map(decodeURIComponent);
const pairKey = pairArr[0].toLowerCase();
const pairVal = pairArr[1];
const pairAcc = acc;
pairAcc[pairKey] = pairVal;
return pairAcc;
}, {});
// 'OTP' will be instantiated with 'config' argument.
let OTP;
const config = {};
if (uriType === 'hotp') {
OTP = HOTP;
// Counter: required
if (typeof uriParams.counter !== 'undefined' && INTEGER_REGEX.test(uriParams.counter)) {
config.counter = parseInt(uriParams.counter, 10);
} else {
throw new TypeError('Missing or invalid \'counter\' parameter');
}
} else if (uriType === 'totp') {
OTP = TOTP;
// Period: optional
if (typeof uriParams.period !== 'undefined') {
if (POSITIVE_INTEGER_REGEX.test(uriParams.period)) {
config.period = parseInt(uriParams.period, 10);
} else {
throw new TypeError('Invalid \'period\' parameter');
}
}
} else {
throw new TypeError('Unknown OTP type');
}
// Label: required
// Issuer: optional
if (uriLabel.length === 2) {
config.label = uriLabel[1];
if (typeof uriParams.issuer === 'undefined') {
config.issuer = uriLabel[0];
} else if (uriParams.issuer === uriLabel[0]) {
config.issuer = uriParams.issuer;
} else {
throw new TypeError('Invalid \'issuer\' parameter');
}
} else {
config.label = uriLabel[0];
if (typeof uriParams.issuer !== 'undefined') {
config.issuer = uriParams.issuer;
}
}
// Secret: required
if (typeof uriParams.secret !== 'undefined' && SECRET_REGEX.test(uriParams.secret)) {
config.secret = new Secret({ buffer: Utils.b32.toBuf(uriParams.secret) });
} else {
throw new TypeError('Missing or invalid \'secret\' parameter');
}
// Algorithm: optional
if (typeof uriParams.algorithm !== 'undefined') {
if (ALGORITHM_REGEX.test(uriParams.algorithm)) {
config.algorithm = uriParams.algorithm;
} else {
throw new TypeError('Invalid \'algorithm\' parameter');
}
}
// Digits: optional
if (typeof uriParams.digits !== 'undefined') {
if (POSITIVE_INTEGER_REGEX.test(uriParams.digits)) {
config.digits = parseInt(uriParams.digits, 10);
} else {
throw new TypeError('Invalid \'digits\' parameter');
}
}
return new OTP(config);
}
/**
* Converts an HOTP/TOTP object to a Google Authenticator key URI.
* @param {HOTP|TOTP} otp HOTP/TOTP object.
* @param {Object} [config] Configuration options.
* @returns {string} Google Authenticator Key URI.
*/
static stringify(otp) {
if (otp instanceof HOTP || otp instanceof TOTP) {
return otp.toString();
}
throw new TypeError('Invalid \'HOTP/TOTP\' object');
}
}