initial commit
[squeep-totp] / lib / totp.js
diff --git a/lib/totp.js b/lib/totp.js
new file mode 100644 (file)
index 0000000..a305193
--- /dev/null
@@ -0,0 +1,77 @@
+'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;