3 const crypto
= require('crypto');
5 class HMACBasedOneTimePassword
{
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
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');
22 if (typeof(this.counter
) !== 'bigint') {
23 this.counter
= BigInt(this.counter
);
27 get _defaultOptions() {
37 get _algorithmKeyLengths() {
45 * @param {String} algorithm
48 _algorithmKeyLength(algorithm
) {
49 if (!(this._algorithmKeyLengths
[algorithm
])) {
50 throw new RangeError(`unsupported algorithm '${algorithm}'`);
52 return this._algorithmKeyLengths
[algorithm
]; // eslint-disable-line security/detect-object-injection
57 * @param {BigInt} count
61 const counterBuffer
= Buffer
.alloc(8);
62 counterBuffer
.writeBigUInt64BE(count
);
63 return crypto
.createHmac(this.algorithm
, this.keyBuffer
)
64 .update(counterBuffer
)
70 * @param {BigInt} count
74 const digest
= this._hmac(count
);
75 const offset
= digest
[digest
.length
- 1] & 0x0f;
76 return digest
.readUInt32BE(offset
) & 0x7fffffff;
81 * @param {BigInt=} 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) {
95 * @param {String} hotp
96 * @param {BigInt=} count
99 validate(hotp
, count
) {
100 const codeString
= this.generate(count
);
101 return codeString
=== hotp
.trim();
106 module
.exports
= HMACBasedOneTimePassword
;