bump package version to 1.1.5
[squeep-totp] / test / totp.js
1 /* eslint-env mocha */
2 'use strict';
3
4 const assert = require('assert');
5 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
6 const TOTP = require('../lib/totp');
7
8 describe('TOTP', function () {
9 let options, totp;
10
11 const tests = [
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' },
30 ];
31
32 const algorithmKeys = {
33 'sha1': Buffer.from('12345678901234567890'),
34 'sha256': Buffer.from('12345678901234567890123456789012'),
35 'sha512': Buffer.from('1234567890123456789012345678901234567890123456789012345678901234'),
36 };
37
38 beforeEach(function () {
39 sinon.restore();
40 options = {
41 codeLength: 8,
42 };
43 });
44
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);
50 });
51
52 it('covers invalid key size', function () {
53 options.algorithm = 'sha1';
54 options.key = Buffer.from('smol');
55 assert.throws(() => new TOTP(options), RangeError);
56 });
57 }); // constructor
58
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);
67 });
68 }
69 it('covers setting counter as nop', function () {
70 options.algorithm = 'sha1';
71 options.key = algorithmKeys[options.algorithm];
72 totp = new TOTP(options);
73 totp.counter = 0;
74 assert.notStrictEqual(totp.counter, 0);
75 });
76 }); // counter
77
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);
86 });
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);
94 });
95 }
96 }); // generate
97
98 describe('validate', function () {
99 const totpOffsets = {
100 '-2': '73751819',
101 '-1': '73039595',
102 '0': '45301026', // oathtool --totp -N 'Sept 29 2023 02:58:00' --digits=8 3132333435363738393031323334353637383930
103 '1': '16636878',
104 '2': '37492012',
105 };
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);
112 });
113 it('validates correct totp', function () {
114 const result = totp.validate(totpOffsets[0]);
115 assert.strictEqual(result, true);
116 });
117 it('validates totp within future window', function () {
118 const result = totp.validate(totpOffsets[1]);
119 assert.strictEqual(result, true);
120 });
121 it('validates totp within past window', function () {
122 const result = totp.validate(totpOffsets[-1]);
123 assert.strictEqual(result, true);
124 });
125 it('fails totp outside future window', function () {
126 const result = totp.validate(totpOffsets[2]);
127 assert.strictEqual(result, false);
128 });
129 it('fails totp outside past window', function () {
130 const result = totp.validate(totpOffsets[-2]);
131 assert.strictEqual(result, false);
132 });
133 it('fails invalid totp', function () {
134 const result = totp.validate('00000000');
135 assert.strictEqual(result, false);
136 });
137 }); // validate
138
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);
147 });
148 }
149 }); // createKey
150
151 describe('createKeySVG', function () {
152 let options, key, keyEncoding;
153 beforeEach(function () {
154 options = {
155 issuer: 'Squeep',
156 accountname: 'test@example.com',
157 };
158 key = algorithmKeys['sha1'];
159 keyEncoding = 'buffer';
160 });
161 it('creates totp type qrcoded uri', function () {
162 const { secret, svg, uri } = TOTP.createKeySVG(options, key, keyEncoding);
163 assert(svg);
164 assert(uri.includes('totp'));
165 assert.strictEqual(secret, 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ');
166 });
167 }); // createKeySVG
168
169 }); // TOTP