/** * @otplib/core * * @author Gerald Yeo * @version: 12.0.1 * @license: MIT **/ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function objectValues(value) { return Object.keys(value).map(key => value[key]); } (function (HashAlgorithms) { HashAlgorithms["SHA1"] = "sha1"; HashAlgorithms["SHA256"] = "sha256"; HashAlgorithms["SHA512"] = "sha512"; })(exports.HashAlgorithms || (exports.HashAlgorithms = {})); const HASH_ALGORITHMS = objectValues(exports.HashAlgorithms); (function (KeyEncodings) { KeyEncodings["ASCII"] = "ascii"; KeyEncodings["BASE64"] = "base64"; KeyEncodings["HEX"] = "hex"; KeyEncodings["LATIN1"] = "latin1"; KeyEncodings["UTF8"] = "utf8"; })(exports.KeyEncodings || (exports.KeyEncodings = {})); const KEY_ENCODINGS = objectValues(exports.KeyEncodings); (function (Strategy) { Strategy["HOTP"] = "hotp"; Strategy["TOTP"] = "totp"; })(exports.Strategy || (exports.Strategy = {})); const STRATEGY = objectValues(exports.Strategy); const createDigestPlaceholder = () => { throw new Error('Please provide an options.createDigest implementation.'); }; function isTokenValid(value) { return /^(\d+)$/.test(value); } function padStart(value, maxLength, fillString) { if (value.length >= maxLength) { return value; } const padding = Array(maxLength + 1).join(fillString); return `${padding}${value}`.slice(-1 * maxLength); } function keyuri(options) { const tmpl = `otpauth://${options.type}/{labelPrefix}:{accountName}?secret={secret}{query}`; const params = []; if (STRATEGY.indexOf(options.type) < 0) { throw new Error(`Expecting options.type to be one of ${STRATEGY.join(', ')}. Received ${options.type}.`); } if (options.type === 'hotp') { if (options.counter == null || typeof options.counter !== 'number') { throw new Error('Expecting options.counter to be a number when options.type is "hotp".'); } params.push(`&counter=${options.counter}`); } if (options.type === 'totp' && options.step) { params.push(`&period=${options.step}`); } if (options.digits) { params.push(`&digits=${options.digits}`); } if (options.algorithm) { params.push(`&algorithm=${options.algorithm.toUpperCase()}`); } if (options.issuer) { params.push(`&issuer=${encodeURIComponent(options.issuer)}`); } return tmpl.replace('{labelPrefix}', encodeURIComponent(options.issuer || options.accountName)).replace('{accountName}', encodeURIComponent(options.accountName)).replace('{secret}', options.secret).replace('{query}', params.join('')); } class OTP { constructor(defaultOptions = {}) { this._defaultOptions = Object.freeze({ ...defaultOptions }); this._options = Object.freeze({}); } create(defaultOptions = {}) { return new OTP(defaultOptions); } clone(defaultOptions = {}) { const instance = this.create({ ...this._defaultOptions, ...defaultOptions }); instance.options = this._options; return instance; } get options() { return Object.freeze({ ...this._defaultOptions, ...this._options }); } set options(options) { this._options = Object.freeze({ ...this._options, ...options }); } allOptions() { return this.options; } resetOptions() { this._options = Object.freeze({}); } } function hotpOptionsValidator(options) { if (typeof options.createDigest !== 'function') { throw new Error('Expecting options.createDigest to be a function.'); } if (typeof options.createHmacKey !== 'function') { throw new Error('Expecting options.createHmacKey to be a function.'); } if (typeof options.digits !== 'number') { throw new Error('Expecting options.digits to be a number.'); } if (!options.algorithm || HASH_ALGORITHMS.indexOf(options.algorithm) < 0) { throw new Error(`Expecting options.algorithm to be one of ${HASH_ALGORITHMS.join(', ')}. Received ${options.algorithm}.`); } if (!options.encoding || KEY_ENCODINGS.indexOf(options.encoding) < 0) { throw new Error(`Expecting options.encoding to be one of ${KEY_ENCODINGS.join(', ')}. Received ${options.encoding}.`); } } const hotpCreateHmacKey = (algorithm, secret, encoding) => { return Buffer.from(secret, encoding).toString('hex'); }; function hotpDefaultOptions() { const options = { algorithm: exports.HashAlgorithms.SHA1, createHmacKey: hotpCreateHmacKey, createDigest: createDigestPlaceholder, digits: 6, encoding: exports.KeyEncodings.ASCII }; return options; } function hotpOptions(opt) { const options = { ...hotpDefaultOptions(), ...opt }; hotpOptionsValidator(options); return Object.freeze(options); } function hotpCounter(counter) { const hexCounter = counter.toString(16); return padStart(hexCounter, 16, '0'); } function hotpDigestToToken(hexDigest, digits) { const digest = Buffer.from(hexDigest, 'hex'); const offset = digest[digest.length - 1] & 0xf; const binary = (digest[offset] & 0x7f) << 24 | (digest[offset + 1] & 0xff) << 16 | (digest[offset + 2] & 0xff) << 8 | digest[offset + 3] & 0xff; const token = binary % Math.pow(10, digits); return padStart(String(token), digits, '0'); } function hotpDigest(secret, counter, options) { const hexCounter = hotpCounter(counter); const hmacKey = options.createHmacKey(options.algorithm, secret, options.encoding); return options.createDigest(options.algorithm, hmacKey, hexCounter); } function hotpToken(secret, counter, options) { const hexDigest = options.digest || hotpDigest(secret, counter, options); return hotpDigestToToken(hexDigest, options.digits); } function hotpCheck(token, secret, counter, options) { if (!isTokenValid(token)) { return false; } const systemToken = hotpToken(secret, counter, options); return token === systemToken; } function hotpKeyuri(accountName, issuer, secret, counter, options) { return keyuri({ algorithm: options.algorithm, digits: options.digits, type: exports.Strategy.HOTP, accountName, counter, issuer, secret }); } class HOTP extends OTP { create(defaultOptions = {}) { return new HOTP(defaultOptions); } allOptions() { return hotpOptions(this.options); } generate(secret, counter) { return hotpToken(secret, counter, this.allOptions()); } check(token, secret, counter) { return hotpCheck(token, secret, counter, this.allOptions()); } verify(opts) { if (typeof opts !== 'object') { throw new Error('Expecting argument 0 of verify to be an object'); } return this.check(opts.token, opts.secret, opts.counter); } keyuri(accountName, issuer, secret, counter) { return hotpKeyuri(accountName, issuer, secret, counter, this.allOptions()); } } function parseWindowBounds(win) { if (typeof win === 'number') { return [Math.abs(win), Math.abs(win)]; } if (Array.isArray(win)) { const [past, future] = win; if (typeof past === 'number' && typeof future === 'number') { return [Math.abs(past), Math.abs(future)]; } } throw new Error('Expecting options.window to be an number or [number, number].'); } function totpOptionsValidator(options) { hotpOptionsValidator(options); parseWindowBounds(options.window); if (typeof options.epoch !== 'number') { throw new Error('Expecting options.epoch to be a number.'); } if (typeof options.step !== 'number') { throw new Error('Expecting options.step to be a number.'); } } const totpPadSecret = (secret, encoding, minLength) => { const currentLength = secret.length; const hexSecret = Buffer.from(secret, encoding).toString('hex'); if (currentLength < minLength) { const newSecret = new Array(minLength - currentLength + 1).join(hexSecret); return Buffer.from(newSecret, 'hex').slice(0, minLength).toString('hex'); } return hexSecret; }; const totpCreateHmacKey = (algorithm, secret, encoding) => { switch (algorithm) { case exports.HashAlgorithms.SHA1: return totpPadSecret(secret, encoding, 20); case exports.HashAlgorithms.SHA256: return totpPadSecret(secret, encoding, 32); case exports.HashAlgorithms.SHA512: return totpPadSecret(secret, encoding, 64); default: throw new Error(`Expecting algorithm to be one of ${HASH_ALGORITHMS.join(', ')}. Received ${algorithm}.`); } }; function totpDefaultOptions() { const options = { algorithm: exports.HashAlgorithms.SHA1, createDigest: createDigestPlaceholder, createHmacKey: totpCreateHmacKey, digits: 6, encoding: exports.KeyEncodings.ASCII, epoch: Date.now(), step: 30, window: 0 }; return options; } function totpOptions(opt) { const options = { ...totpDefaultOptions(), ...opt }; totpOptionsValidator(options); return Object.freeze(options); } function totpCounter(epoch, step) { return Math.floor(epoch / step / 1000); } function totpToken(secret, options) { const counter = totpCounter(options.epoch, options.step); return hotpToken(secret, counter, options); } function totpEpochsInWindow(epoch, direction, deltaPerEpoch, numOfEpoches) { const result = []; if (numOfEpoches === 0) { return result; } for (let i = 1; i <= numOfEpoches; i++) { const delta = direction * i * deltaPerEpoch; result.push(epoch + delta); } return result; } function totpEpochAvailable(epoch, step, win) { const bounds = parseWindowBounds(win); const delta = step * 1000; return { current: epoch, past: totpEpochsInWindow(epoch, -1, delta, bounds[0]), future: totpEpochsInWindow(epoch, 1, delta, bounds[1]) }; } function totpCheck(token, secret, options) { if (!isTokenValid(token)) { return false; } const systemToken = totpToken(secret, options); return token === systemToken; } function totpCheckByEpoch(epochs, token, secret, options) { let position = null; epochs.some((epoch, idx) => { if (totpCheck(token, secret, { ...options, epoch })) { position = idx + 1; return true; } return false; }); return position; } function totpCheckWithWindow(token, secret, options) { if (totpCheck(token, secret, options)) { return 0; } const epochs = totpEpochAvailable(options.epoch, options.step, options.window); const backward = totpCheckByEpoch(epochs.past, token, secret, options); if (backward !== null) { return backward * -1; } return totpCheckByEpoch(epochs.future, token, secret, options); } function totpTimeUsed(epoch, step) { return Math.floor(epoch / 1000) % step; } function totpTimeRemaining(epoch, step) { return step - totpTimeUsed(epoch, step); } function totpKeyuri(accountName, issuer, secret, options) { return keyuri({ algorithm: options.algorithm, digits: options.digits, step: options.step, type: exports.Strategy.TOTP, accountName, issuer, secret }); } class TOTP extends HOTP { create(defaultOptions = {}) { return new TOTP(defaultOptions); } allOptions() { return totpOptions(this.options); } generate(secret) { return totpToken(secret, this.allOptions()); } checkDelta(token, secret) { return totpCheckWithWindow(token, secret, this.allOptions()); } check(token, secret) { const delta = this.checkDelta(token, secret); return typeof delta === 'number'; } verify(opts) { if (typeof opts !== 'object') { throw new Error('Expecting argument 0 of verify to be an object'); } return this.check(opts.token, opts.secret); } timeRemaining() { const options = this.allOptions(); return totpTimeRemaining(options.epoch, options.step); } timeUsed() { const options = this.allOptions(); return totpTimeUsed(options.epoch, options.step); } keyuri(accountName, issuer, secret) { return totpKeyuri(accountName, issuer, secret, this.allOptions()); } } function authenticatorOptionValidator(options) { totpOptionsValidator(options); if (typeof options.keyDecoder !== 'function') { throw new Error('Expecting options.keyDecoder to be a function.'); } if (options.keyEncoder && typeof options.keyEncoder !== 'function') { throw new Error('Expecting options.keyEncoder to be a function.'); } } function authenticatorDefaultOptions() { const options = { algorithm: exports.HashAlgorithms.SHA1, createDigest: createDigestPlaceholder, createHmacKey: totpCreateHmacKey, digits: 6, encoding: exports.KeyEncodings.HEX, epoch: Date.now(), step: 30, window: 0 }; return options; } function authenticatorOptions(opt) { const options = { ...authenticatorDefaultOptions(), ...opt }; authenticatorOptionValidator(options); return Object.freeze(options); } function authenticatorEncoder(secret, options) { return options.keyEncoder(secret, options.encoding); } function authenticatorDecoder(secret, options) { return options.keyDecoder(secret, options.encoding); } function authenticatorGenerateSecret(numberOfBytes, options) { const key = options.createRandomBytes(numberOfBytes, options.encoding); return authenticatorEncoder(key, options); } function authenticatorToken(secret, options) { return totpToken(authenticatorDecoder(secret, options), options); } function authenticatorCheckWithWindow(token, secret, options) { return totpCheckWithWindow(token, authenticatorDecoder(secret, options), options); } class Authenticator extends TOTP { create(defaultOptions = {}) { return new Authenticator(defaultOptions); } allOptions() { return authenticatorOptions(this.options); } generate(secret) { return authenticatorToken(secret, this.allOptions()); } checkDelta(token, secret) { return authenticatorCheckWithWindow(token, secret, this.allOptions()); } encode(secret) { return authenticatorEncoder(secret, this.allOptions()); } decode(secret) { return authenticatorDecoder(secret, this.allOptions()); } generateSecret(numberOfBytes = 10) { return authenticatorGenerateSecret(numberOfBytes, this.allOptions()); } } exports.Authenticator = Authenticator; exports.HASH_ALGORITHMS = HASH_ALGORITHMS; exports.HOTP = HOTP; exports.KEY_ENCODINGS = KEY_ENCODINGS; exports.OTP = OTP; exports.STRATEGY = STRATEGY; exports.TOTP = TOTP; exports.authenticatorCheckWithWindow = authenticatorCheckWithWindow; exports.authenticatorDecoder = authenticatorDecoder; exports.authenticatorDefaultOptions = authenticatorDefaultOptions; exports.authenticatorEncoder = authenticatorEncoder; exports.authenticatorGenerateSecret = authenticatorGenerateSecret; exports.authenticatorOptionValidator = authenticatorOptionValidator; exports.authenticatorOptions = authenticatorOptions; exports.authenticatorToken = authenticatorToken; exports.createDigestPlaceholder = createDigestPlaceholder; exports.hotpCheck = hotpCheck; exports.hotpCounter = hotpCounter; exports.hotpCreateHmacKey = hotpCreateHmacKey; exports.hotpDefaultOptions = hotpDefaultOptions; exports.hotpDigestToToken = hotpDigestToToken; exports.hotpKeyuri = hotpKeyuri; exports.hotpOptions = hotpOptions; exports.hotpOptionsValidator = hotpOptionsValidator; exports.hotpToken = hotpToken; exports.isTokenValid = isTokenValid; exports.keyuri = keyuri; exports.objectValues = objectValues; exports.padStart = padStart; exports.totpCheck = totpCheck; exports.totpCheckByEpoch = totpCheckByEpoch; exports.totpCheckWithWindow = totpCheckWithWindow; exports.totpCounter = totpCounter; exports.totpCreateHmacKey = totpCreateHmacKey; exports.totpDefaultOptions = totpDefaultOptions; exports.totpEpochAvailable = totpEpochAvailable; exports.totpKeyuri = totpKeyuri; exports.totpOptions = totpOptions; exports.totpOptionsValidator = totpOptionsValidator; exports.totpPadSecret = totpPadSecret; exports.totpTimeRemaining = totpTimeRemaining; exports.totpTimeUsed = totpTimeUsed; exports.totpToken = totpToken;