totp.test.ts

2.4 KB · raw

1// Covers §24's two-step sign-in: RFC 6238 itself, the step either side of now, and what is refused.23import { describe, expect, test } from "bun:test";4import { codeAt, DIGITS, isSecret, newSecret, stepFor } from "../src/server/totp.ts";56// RFC 6238's own secret and the 8-digit codes its appendix B publishes. The six-digit ones are the7// same dynamic truncation with the rest of the number dropped, checked against an hour of Python.8const SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ";9const VECTORS: [at: number, code: string, step: number][] = [10	[59, "287082", 1],11	[1111111109, "081804", 37037036],12	[1234567890, "005924", 41152263],13	[2000000000, "279037", 66666666],14];1516describe("TOTP codes (§24)", () => {17	test("matches the RFC's vectors, at the step they belong to", () => {18		for (const [at, code, step] of VECTORS) {19			expect(stepFor(SECRET, code, at)).toBe(step);20			expect(codeAt(SECRET, at)).toBe(code);21		}22	});2324	test("accepts a step either side of now, and nothing further", () => {25		const [at, code] = VECTORS[0] as [number, string, number];26		expect(stepFor(SECRET, code, at + 30)).toBe(1);27		expect(stepFor(SECRET, code, at - 30)).toBe(1);28		expect(stepFor(SECRET, code, at + 60)).toBeNull();29		expect(stepFor(SECRET, code, at - 61)).toBeNull();30	});3132	test("a code is only right for its own secret and its own shape", () => {33		const other = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP";34		expect(stepFor(other, "287082", 59)).toBeNull();35		expect(stepFor(SECRET, "28708", 59)).toBeNull();36		expect(stepFor(SECRET, "2870821", 59)).toBeNull();37		expect(stepFor(SECRET, "abcdef", 59)).toBeNull();38		expect(stepFor(SECRET, "", 59)).toBeNull();39		expect(stepFor("not base32!", "287082", 59)).toBeNull();40	});4142	test("a fresh secret is base32 of the length authenticators use", () => {43		const secret = newSecret();44		expect(isSecret(secret)).toBe(true);45		expect(secret).toHaveLength(32);46		expect(secret).toMatch(/^[A-Z2-7]+$/);47		expect(secret).not.toBe(newSecret());48		expect(codeAt(secret, 1111111109)).toHaveLength(DIGITS);49	});5051	test("a secret too short to be worth having is refused, as is anything that is not base32", () => {52		// 128 bits is the shortest RFC 4226 recommends, so 26 base32 characters.53		expect(isSecret("ABCD")).toBe(false);54		expect(isSecret("OOOO")).toBe(false);55		expect(isSecret("")).toBe(false);56		expect(isSecret("JBSWY3DPEHPK3PXP")).toBe(false);57		expect(isSecret("not base32!")).toBe(false);58		expect(isSecret(SECRET)).toBe(true);59	});60});