--- /dev/null
+'use strict';
+
+const HOTP = require('./hotp');
+
+class TimeBasedOneTimePassword extends HOTP {
+ /**
+ *
+ * @param {Object} options
+ * @param {Number} options.codeLength
+ * @param {Buffer|String} options.key
+ * @param {String} options.keyEncoding
+ * @param {String} options.algorithm
+ * @param {Number} options.timeStepSeconds
+ * @param {Number} options.timeStepStartSeconds
+ * @param {Number} options.driftForward
+ * @param {Number} options.driftBackward
+ */
+ 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)),
+ ];
+ }
+
+ get _algorithmKeyLengths() {
+ return {
+ ...super._algorithmKeyLengths,
+ 'sha256': 32,
+ 'sha512': 64,
+ };
+ }
+
+ get counter() {
+ const epoch = Math.floor(Date.now() / 1000);
+ return BigInt(Math.floor((epoch - this.timeStepStartSeconds) / this.timeStepSeconds));
+ }
+
+ set counter(_) { /* */ }
+
+ get _defaultOptions() {
+ const options = Object.assign(super._defaultOptions, {
+ timeStepSeconds: 30,
+ timeStepStartSeconds: 0,
+ driftForward: 1,
+ driftBackward: 1,
+ });
+ delete options.counter;
+ return options;
+ }
+
+ generate(count = this.counter) {
+ return super.generate(count);
+ }
+
+ /**
+ *
+ * @param {String} hotp
+ * @param {BigInt=} count
+ * @returns {Boolean}
+ */
+ validate(hotp, count) {
+ const counter = count ?? this.counter;
+ for (const offset of this.driftOffsets) {
+ const codeString = this.generate(counter + offset);
+ if (codeString === hotp.trim()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
+
+module.exports = TimeBasedOneTimePassword;