UNPKG

@squeep/totp

Version:

A minimal TOTP/HOTP helper.

98 lines (86 loc) 2.63 kB
'use strict'; const HOTP = require('./hotp'); const crypto = require('node:crypto'); class TimeBasedOneTimePassword extends HOTP { /** * * @param {object} options options * @param {number} options.codeLength digits in code * @param {Buffer|string} options.key secret key * @param {string} options.keyEncoding secret key encoding * @param {string} options.algorithm algorithm * @param {number} options.timeStepSeconds seconds per increment * @param {number} options.timeStepStartSeconds seconds offset * @param {number} options.driftForward allowed future steps to check * @param {number} options.driftBackward allowed past steps to check */ constructor(options) { const _options = { ...options }; super(_options); this.driftOffsets = [ 0n, // Check now first ...Array.from({ length: this.driftBackward }, (v, k) => BigInt(-(k + 1))), ...Array.from({ length: this.driftForward }, (v, k) => BigInt(k + 1)), ]; } static get _algorithmKeyLengths() { return { ...super._algorithmKeyLengths, 'sha256': 32, 'sha512': 64, }; } /** * The type used when constructing the otpauth URI. * @returns {string} otp auth type */ static get _type() { return 'totp'; } /** * Derive counter from epoch. * @returns {bigint} time based counter */ get counter() { const epoch = Math.floor(Date.now() / 1000); return BigInt(Math.floor((epoch - this.timeStepStartSeconds) / this.timeStepSeconds)); } set counter(_) { /* Ignore assignment */ } // eslint-disable-line class-methods-use-this static get _defaultOptions() { const options = Object.assign(super._defaultOptions, { timeStepSeconds: 30, timeStepStartSeconds: 0, driftForward: 1, driftBackward: 1, }); delete options.counter; return options; } /** * * @param {bigint=} count counter value * @returns {string} code */ generate(count = this.counter) { return super.generate(count); } /** * * @param {string} hotp code * @param {bigint=} count counter value * @returns {boolean} is valid */ validate(hotp, count) { const counter = count ?? this.counter; const hotpB = Buffer.from(hotp.trim()); for (const offset of this.driftOffsets) { const codeString = this.generate(counter + offset); const codeStringB = Buffer.from(codeString); if (hotpB.length === codeString.length && crypto.timingSafeEqual(hotpB, codeStringB)) { return true; } } return false; } } module.exports = TimeBasedOneTimePassword;