@squeep/totp
Version:
A minimal TOTP/HOTP helper.
98 lines (86 loc) • 2.63 kB
JavaScript
;
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;