initial commit
[squeep-totp] / lib / hotp.js
1 'use strict';
2
3 const crypto = require('crypto');
4
5 class HMACBasedOneTimePassword {
6 /**
7 *
8 * @param {Object} options
9 * @param {Number=} options.codeLength
10 * @param {BigInt|Number|String=} options.counter
11 * @param {Buffer|String} options.key
12 * @param {String=} options.keyEncoding
13 * @param {String=} options.algorithm
14 */
15 constructor(options) {
16 Object.assign(this, this._defaultOptions, options);
17 this.keyBuffer = Buffer.isBuffer(this.key) ? this.key : Buffer.from(this.key, this.keyEncoding);
18 const expectedKeyLength = this._algorithmKeyLength(this.algorithm);
19 if (this.keyBuffer.length !== expectedKeyLength) {
20 throw new RangeError('key size does not match algorithm');
21 }
22 if (typeof(this.counter) !== 'bigint') {
23 this.counter = BigInt(this.counter);
24 }
25 }
26
27 get _defaultOptions() {
28 return {
29 codeLength: 6,
30 counter: 0n,
31 key: undefined,
32 keyEncoding: 'ascii',
33 algorithm: 'sha1',
34 };
35 }
36
37 get _algorithmKeyLengths() {
38 return {
39 'sha1': 20,
40 };
41 }
42
43 /**
44 *
45 * @param {String} algorithm
46 * @returns {Number}
47 */
48 _algorithmKeyLength(algorithm) {
49 if (!(this._algorithmKeyLengths[algorithm])) {
50 throw new RangeError(`unsupported algorithm '${algorithm}'`);
51 }
52 return this._algorithmKeyLengths[algorithm]; // eslint-disable-line security/detect-object-injection
53 }
54
55 /**
56 *
57 * @param {BigInt} count
58 * @returns {Buffer}
59 */
60 _hmac(count) {
61 const counterBuffer = Buffer.alloc(8);
62 counterBuffer.writeBigUInt64BE(count);
63 return crypto.createHmac(this.algorithm, this.keyBuffer)
64 .update(counterBuffer)
65 .digest();
66 }
67
68 /**
69 *
70 * @param {BigInt} count
71 * @returns {Number}
72 */
73 _truncate(count) {
74 const digest = this._hmac(count);
75 const offset = digest[digest.length - 1] & 0x0f;
76 return digest.readUInt32BE(offset) & 0x7fffffff;
77 }
78
79 /**
80 *
81 * @param {BigInt=} count
82 * @returns {String}
83 */
84 generate(count) {
85 const code = this._truncate(count ?? this.counter);
86 const codeString = ('0'.repeat(this.codeLength + 1) + code.toString(10)).slice(0 - this.codeLength);
87 if (count === undefined) {
88 this.counter += 1n;
89 }
90 return codeString;
91 }
92
93 /**
94 *
95 * @param {String} hotp
96 * @param {BigInt=} count
97 * @returns {Boolean}
98 */
99 validate(hotp, count) {
100 const codeString = this.generate(count);
101 return codeString === hotp.trim();
102 }
103
104 }
105
106 module.exports = HMACBasedOneTimePassword;