import { describe, expect, it } from "vitest"; import { utf8ToBytes } from "../../src/crypto/encoding"; import { hotp, totp, totpCounter, verifyHotp, verifyTotp, } from "../../src/otp/otp"; describe("HOTP", () => { it("matches every RFC 4226 test value", async () => { const secret = utf8ToBytes("12345678901234567890"); const expected = [ "755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871", "520489", ]; await expect( Promise.all( expected.map((_, counter) => hotp({ secret, counter: BigInt(counter) }), ), ), ).resolves.toEqual(expected); }); it("searches forward without losing leading zeroes", async () => { const secret = utf8ToBytes("12345678901234567890"); const match = await verifyHotp("338314", { secret, counter: 1n, lookAhead: 5, }); expect(match).toMatchObject({ counter: 4n, delta: 3 }); }); }); describe("TOTP", () => { const cases = [ [59, "94287082", "46119246", "90693936"], [1_111_111_109, "07081804", "68084774", "25091201"], [1_111_111_111, "14050471", "67062674", "99943326"], [1_234_567_890, "89005924", "91819424", "93441116"], [2_000_000_000, "69279037", "90698825", "38618901"], [20_000_000_000, "65353130", "77737706", "47863826"], ] as const; it("matches RFC 6238 SHA-1, SHA-256 and SHA-512 vectors", async () => { const secrets = { "SHA-1": utf8ToBytes("12345678901234567890"), "SHA-256": utf8ToBytes("12345678901234567890123456789012"), "SHA-512": utf8ToBytes( "1234567890123456789012345678901234567890123456789012345678901234", ), } as const; for (const [timestamp, sha1, sha256, sha512] of cases) { await expect( totp({ secret: secrets["SHA-1"], timestamp, digits: 8, algorithm: "SHA-1", }), ).resolves.toBe(sha1); await expect( totp({ secret: secrets["SHA-256"], timestamp, digits: 8, algorithm: "SHA-256", }), ).resolves.toBe(sha256); await expect( totp({ secret: secrets["SHA-512"], timestamp, digits: 8, algorithm: "SHA-512", }), ).resolves.toBe(sha512); } }); it("uses integer counters beyond 2038", () => { expect(totpCounter(20_000_000_000)).toBe(666_666_666n); }); it("diagnoses a bounded clock delta", async () => { const secret = utf8ToBytes("12345678901234567890"); const code = await totp({ secret, timestamp: 1_111_111_109 }); const match = await verifyTotp(code, { secret, timestamp: 1_111_111_109 + 60, window: 3, }); expect(match?.delta).toBe(-2); }); });