4 const assert
= require('assert');
5 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
6 const TOTP
= require('../lib/totp');
8 describe('TOTP', function () {
12 { epoch: 59, count: BigInt(0x1), algorithm: 'sha1', totp: '94287082' },
13 { epoch: 59, count: BigInt(0x1), algorithm: 'sha256', totp: '46119246' },
14 { epoch: 59, count: BigInt(0x1), algorithm: 'sha512', totp: '90693936' },
15 { epoch: 1111111109, count: BigInt(0x23523EC), algorithm: 'sha1', totp: '07081804' },
16 { epoch: 1111111109, count: BigInt(0x23523EC), algorithm: 'sha256', totp: '68084774' },
17 { epoch: 1111111109, count: BigInt(0x23523EC), algorithm: 'sha512', totp: '25091201' },
18 { epoch: 1111111111, count: BigInt(0x23523ED), algorithm: 'sha1', totp: '14050471' },
19 { epoch: 1111111111, count: BigInt(0x23523ED), algorithm: 'sha256', totp: '67062674' },
20 { epoch: 1111111111, count: BigInt(0x23523ED), algorithm: 'sha512', totp: '99943326' },
21 { epoch: 1234567890, count: BigInt(0x273EF07), algorithm: 'sha1', totp: '89005924' },
22 { epoch: 1234567890, count: BigInt(0x273EF07), algorithm: 'sha256', totp: '91819424' },
23 { epoch: 1234567890, count: BigInt(0x273EF07), algorithm: 'sha512', totp: '93441116' },
24 { epoch: 2000000000, count: BigInt(0x3F940AA), algorithm: 'sha1', totp: '69279037' },
25 { epoch: 2000000000, count: BigInt(0x3F940AA), algorithm: 'sha256', totp: '90698825' },
26 { epoch: 2000000000, count: BigInt(0x3F940AA), algorithm: 'sha512', totp: '38618901' },
27 { epoch: 20000000000, count: BigInt(0x27BC86AA), algorithm: 'sha1', totp: '65353130' },
28 { epoch: 20000000000, count: BigInt(0x27BC86AA), algorithm: 'sha256', totp: '77737706' },
29 { epoch: 20000000000, count: BigInt(0x27BC86AA), algorithm: 'sha512', totp: '47863826' },
32 const algorithmKeys
= {
33 'sha1': Buffer
.from('12345678901234567890'),
34 'sha256': Buffer
.from('12345678901234567890123456789012'),
35 'sha512': Buffer
.from('1234567890123456789012345678901234567890123456789012345678901234'),
38 beforeEach(function () {
45 describe('constructor', function () {
46 it('covers invalid algorithm', function () {
47 options
.algorithm
= 'md5';
48 options
.key
= Buffer
.from('');
49 assert
.throws(() => new TOTP(options
), RangeError
);
52 it('covers invalid key size', function () {
53 options
.algorithm
= 'sha1';
54 options
.key
= Buffer
.from('smol');
55 assert
.throws(() => new TOTP(options
), RangeError
);
59 describe('counter', function () {
60 for (const test
of tests
) {
61 it(`epoch ${test.epoch}`, function () {
62 sinon
.stub(Date
, 'now').returns(test
.epoch
* 1000);
63 options
.algorithm
= test
.algorithm
;
64 options
.key
= algorithmKeys
[test
.algorithm
];
65 totp
= new TOTP(options
);
66 assert
.strictEqual(totp
.counter
, test
.count
);
69 it('covers setting counter as nop', function () {
70 options
.algorithm
= 'sha1';
71 options
.key
= algorithmKeys
[options
.algorithm
];
72 totp
= new TOTP(options
);
74 assert
.notStrictEqual(totp
.counter
, 0);
78 describe('generate', function () {
79 for (const test
of tests
) {
80 it(`count ${test.count} ${test.algorithm}`, function () {
81 options
.algorithm
= test
.algorithm
;
82 options
.key
= algorithmKeys
[test
.algorithm
];
83 totp
= new TOTP(options
);
84 const result
= totp
.generate(test
.count
);
85 assert
.strictEqual(result
, test
.totp
);
87 it(`epoch ${test.epoch} ${test.algorithm}`, function () {
88 sinon
.stub(Date
, 'now').returns(test
.epoch
* 1000);
89 options
.algorithm
= test
.algorithm
;
90 options
.key
= algorithmKeys
[test
.algorithm
];
91 totp
= new TOTP(options
);
92 const result
= totp
.generate();
93 assert
.strictEqual(result
, test
.totp
);
98 describe('validate', function () {
102 '0': '45301026', // oathtool --totp -N 'Sept 29 2023 02:58:00' --digits=8 3132333435363738393031323334353637383930
106 beforeEach(function () {
107 const when
= new Date('2023-09-29T09:58:00.000Z');
108 sinon
.stub(Date
, 'now').returns(when
.getTime());
109 options
.algorithm
= 'sha1';
110 options
.key
= algorithmKeys
['sha1'];
111 totp
= new TOTP(options
);
113 it('validates correct totp', function () {
114 const result
= totp
.validate(totpOffsets
[0]);
115 assert
.strictEqual(result
, true);
117 it('validates totp within future window', function () {
118 const result
= totp
.validate(totpOffsets
[1]);
119 assert
.strictEqual(result
, true);
121 it('validates totp within past window', function () {
122 const result
= totp
.validate(totpOffsets
[-1]);
123 assert
.strictEqual(result
, true);
125 it('fails totp outside future window', function () {
126 const result
= totp
.validate(totpOffsets
[2]);
127 assert
.strictEqual(result
, false);
129 it('fails totp outside past window', function () {
130 const result
= totp
.validate(totpOffsets
[-2]);
131 assert
.strictEqual(result
, false);
133 it('fails invalid totp', function () {
134 const result
= totp
.validate('00000000');
135 assert
.strictEqual(result
, false);
139 describe('createKey', function () {
140 for (const [algorithm
, keySize
] of Object
.entries(TOTP
._algorithmKeyLengths
)) {
141 it(`creates a ${algorithm} key`, async
function () {
142 options
.algorithm
= algorithm
;
143 options
.key
= algorithmKeys
[algorithm
]; // eslint-disable-line security/detect-object-injection
144 const expected
= keySize
* 2; // hex encoding
145 const key
= await TOTP
.createKey(algorithm
);
146 assert
.strictEqual(key
.length
, expected
);
151 describe('createKeySVG', function () {
152 let options
, key
, keyEncoding
;
153 beforeEach(function () {
156 accountname: 'test@example.com',
158 key
= algorithmKeys
['sha1'];
159 keyEncoding
= 'buffer';
161 it('creates totp type qrcoded uri', function () {
162 const { secret
, svg
, uri
} = TOTP
.createKeySVG(options
, key
, keyEncoding
);
164 assert(uri
.includes('totp'));
165 assert
.strictEqual(secret
, 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ');