--- /dev/null
+'use strict';
+
+const crypto = require('crypto');
+
+class HMACBasedOneTimePassword {
+ /**
+ *
+ * @param {Object} options
+ * @param {Number=} options.codeLength
+ * @param {BigInt|Number|String=} options.counter
+ * @param {Buffer|String} options.key
+ * @param {String=} options.keyEncoding
+ * @param {String=} options.algorithm
+ */
+ constructor(options) {
+ Object.assign(this, this._defaultOptions, options);
+ this.keyBuffer = Buffer.isBuffer(this.key) ? this.key : Buffer.from(this.key, this.keyEncoding);
+ const expectedKeyLength = this._algorithmKeyLength(this.algorithm);
+ if (this.keyBuffer.length !== expectedKeyLength) {
+ throw new RangeError('key size does not match algorithm');
+ }
+ if (typeof(this.counter) !== 'bigint') {
+ this.counter = BigInt(this.counter);
+ }
+ }
+
+ get _defaultOptions() {
+ return {
+ codeLength: 6,
+ counter: 0n,
+ key: undefined,
+ keyEncoding: 'ascii',
+ algorithm: 'sha1',
+ };
+ }
+
+ get _algorithmKeyLengths() {
+ return {
+ 'sha1': 20,
+ };
+ }
+
+ /**
+ *
+ * @param {String} algorithm
+ * @returns {Number}
+ */
+ _algorithmKeyLength(algorithm) {
+ if (!(this._algorithmKeyLengths[algorithm])) {
+ throw new RangeError(`unsupported algorithm '${algorithm}'`);
+ }
+ return this._algorithmKeyLengths[algorithm]; // eslint-disable-line security/detect-object-injection
+ }
+
+ /**
+ *
+ * @param {BigInt} count
+ * @returns {Buffer}
+ */
+ _hmac(count) {
+ const counterBuffer = Buffer.alloc(8);
+ counterBuffer.writeBigUInt64BE(count);
+ return crypto.createHmac(this.algorithm, this.keyBuffer)
+ .update(counterBuffer)
+ .digest();
+ }
+
+ /**
+ *
+ * @param {BigInt} count
+ * @returns {Number}
+ */
+ _truncate(count) {
+ const digest = this._hmac(count);
+ const offset = digest[digest.length - 1] & 0x0f;
+ return digest.readUInt32BE(offset) & 0x7fffffff;
+ }
+
+ /**
+ *
+ * @param {BigInt=} count
+ * @returns {String}
+ */
+ generate(count) {
+ const code = this._truncate(count ?? this.counter);
+ const codeString = ('0'.repeat(this.codeLength + 1) + code.toString(10)).slice(0 - this.codeLength);
+ if (count === undefined) {
+ this.counter += 1n;
+ }
+ return codeString;
+ }
+
+ /**
+ *
+ * @param {String} hotp
+ * @param {BigInt=} count
+ * @returns {Boolean}
+ */
+ validate(hotp, count) {
+ const codeString = this.generate(count);
+ return codeString === hotp.trim();
+ }
+
+}
+
+module.exports = HMACBasedOneTimePassword;
\ No newline at end of file