import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { base32Decode, totp } from '../src/core/totp.ts'; /** * RFC 6238's own SHA-1 vectors, whose key is the ASCII string * "12345678901234567890" — base32 below. They validate the decode as much as * the code: a wrong base32 table would not reproduce a single one of them. */ const RFC_KEY = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; describe('totp', () => { it('reproduces the RFC 6238 vectors', () => { // Each vector's 8-digit code, truncated to the 6 digits WebUntis wants. const vectors: [seconds: number, code: string][] = [ [59, '287082'], [1_111_111_109, '081804'], [1_111_111_111, '050471'], [1_234_567_890, '005924'], [2_000_000_000, '279037'], [20_000_000_000, '353130'], ]; for (const [seconds, code] of vectors) { assert.equal(totp(RFC_KEY, seconds * 1000), code, `at ${seconds}s`); } }); it('keeps a leading zero, which is why the code travels as a string', () => { const code = totp(RFC_KEY, 1_234_567_890_000); assert.equal(code, '005924'); assert.equal(code.length, 6); // The bug this guards: sending the code as a JSON number would make it 5924. assert.notEqual(String(Number(code)), code); }); it('changes with the 30-second step and not within it', () => { const base = 1_700_000_000_000; assert.equal(totp(RFC_KEY, base), totp(RFC_KEY, base + 29_000 - (base % 30_000))); assert.notEqual(totp(RFC_KEY, base), totp(RFC_KEY, base + 30_000)); }); }); describe('base32Decode', () => { it('decodes to the bytes behind the RFC key', () => { assert.equal(base32Decode(RFC_KEY).toString('utf8'), '12345678901234567890'); }); it('accepts lowercase, padding and spaces, as a copied key arrives', () => { const expected = base32Decode('JBSWY3DP'); assert.deepEqual(base32Decode('jbswy3dp'), expected); assert.deepEqual(base32Decode('JBSW Y3DP'), expected); assert.deepEqual(base32Decode('JBSWY3DP===='), expected); }); it('refuses a value that is not base32 rather than deriving a wrong code', () => { assert.throws(() => base32Decode('nope!'), /outside A-Z and 2-7/); assert.throws(() => base32Decode(' '), /empty/); }); });