// Covers §24's two-step sign-in: RFC 6238 itself, the step either side of now, and what is refused. import { describe, expect, test } from "bun:test"; import { codeAt, DIGITS, isSecret, newSecret, stepFor } from "../src/server/totp.ts"; // RFC 6238's own secret and the 8-digit codes its appendix B publishes. The six-digit ones are the // same dynamic truncation with the rest of the number dropped, checked against an hour of Python. const SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; const VECTORS: [at: number, code: string, step: number][] = [ [59, "287082", 1], [1111111109, "081804", 37037036], [1234567890, "005924", 41152263], [2000000000, "279037", 66666666], ]; describe("TOTP codes (§24)", () => { test("matches the RFC's vectors, at the step they belong to", () => { for (const [at, code, step] of VECTORS) { expect(stepFor(SECRET, code, at)).toBe(step); expect(codeAt(SECRET, at)).toBe(code); } }); test("accepts a step either side of now, and nothing further", () => { const [at, code] = VECTORS[0] as [number, string, number]; expect(stepFor(SECRET, code, at + 30)).toBe(1); expect(stepFor(SECRET, code, at - 30)).toBe(1); expect(stepFor(SECRET, code, at + 60)).toBeNull(); expect(stepFor(SECRET, code, at - 61)).toBeNull(); }); test("a code is only right for its own secret and its own shape", () => { const other = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"; expect(stepFor(other, "287082", 59)).toBeNull(); expect(stepFor(SECRET, "28708", 59)).toBeNull(); expect(stepFor(SECRET, "2870821", 59)).toBeNull(); expect(stepFor(SECRET, "abcdef", 59)).toBeNull(); expect(stepFor(SECRET, "", 59)).toBeNull(); expect(stepFor("not base32!", "287082", 59)).toBeNull(); }); test("a fresh secret is base32 of the length authenticators use", () => { const secret = newSecret(); expect(isSecret(secret)).toBe(true); expect(secret).toHaveLength(32); expect(secret).toMatch(/^[A-Z2-7]+$/); expect(secret).not.toBe(newSecret()); expect(codeAt(secret, 1111111109)).toHaveLength(DIGITS); }); test("a secret too short to be worth having is refused, as is anything that is not base32", () => { // 128 bits is the shortest RFC 4226 recommends, so 26 base32 characters. expect(isSecret("ABCD")).toBe(false); expect(isSecret("OOOO")).toBe(false); expect(isSecret("")).toBe(false); expect(isSecret("JBSWY3DPEHPK3PXP")).toBe(false); expect(isSecret("not base32!")).toBe(false); expect(isSecret(SECRET)).toBe(true); }); });