12345678910import { describe, expect, it } from "vitest";11import { TOTP, Secret } from "otpauth";1213141516function asciiToHex(s: string): string {17 return Array.from(s).map(c => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");18}1920const RFC_SECRET_SHA1 = Secret.fromHex(asciiToHex("12345678901234567890"));21const RFC_SECRET_SHA256 = Secret.fromHex(asciiToHex("12345678901234567890123456789012"));22const RFC_SECRET_SHA512 = Secret.fromHex(asciiToHex("1234567890123456789012345678901234567890123456789012345678901234"));232425function generateAt(secret: Secret, algorithm: string, digits: number, period: number, t: number): string {26 const totp = new TOTP({ secret, algorithm, digits, period });27 return totp.generate({ timestamp: t * 1000 });28}2930describe("T-004: TOTP RFC 6238 known-answer vectors", () => {31 32 33 34 35 36 37 describe("T-004a: RFC 6238 SHA-1 vectors (8 digits, 30s period)", () => {38 const sha1Vectors: Array<{ t: number; expected: string }> = [39 { t: 59, expected: "94287082" },40 { t: 1111111109, expected: "07081804" },41 { t: 1111111111, expected: "14050471" },42 { t: 1234567890, expected: "89005924" },43 { t: 2000000000, expected: "69279037" },44 { t: 20000000000, expected: "65353130" },45 ];4647 for (const { t, expected } of sha1Vectors) {48 it(`t=${t} → ${expected}`, () => {49 const code = generateAt(RFC_SECRET_SHA1, "SHA1", 8, 30, t);50 expect(code).toBe(expected);51 });52 }53 });5455 56 57 describe("T-004a: RFC 6238 SHA-256 vectors (8 digits, 30s period)", () => {58 const sha256Vectors: Array<{ t: number; expected: string }> = [59 { t: 59, expected: "46119246" },60 { t: 1111111109, expected: "68084774" },61 { t: 1111111111, expected: "67062674" },62 { t: 1234567890, expected: "91819424" },63 { t: 2000000000, expected: "90698825" },64 { t: 20000000000, expected: "77737706" },65 ];6667 for (const { t, expected } of sha256Vectors) {68 it(`t=${t} → ${expected}`, () => {69 const code = generateAt(RFC_SECRET_SHA256, "SHA256", 8, 30, t);70 expect(code).toBe(expected);71 });72 }73 });7475 76 describe("T-004a: RFC 6238 SHA-512 vectors (8 digits, 30s period)", () => {77 const sha512Vectors: Array<{ t: number; expected: string }> = [78 { t: 59, expected: "90693936" },79 { t: 1111111109, expected: "25091201" },80 { t: 1111111111, expected: "99943326" },81 { t: 1234567890, expected: "93441116" },82 { t: 2000000000, expected: "38618901" },83 { t: 20000000000, expected: "47863826" },84 ];8586 for (const { t, expected } of sha512Vectors) {87 it(`t=${t} → ${expected}`, () => {88 const code = generateAt(RFC_SECRET_SHA512, "SHA512", 8, 30, t);89 expect(code).toBe(expected);90 });91 }92 });9394 95 96 97 98 99 it("T-004b: period-boundary roll — t=29 and t=30 produce different codes", () => {100 const code29 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 30, 29);101 const code30 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 30, 30);102 expect(code29).not.toBe(code30);103 });104105 106 107 108 it("T-004c: parseOtpauthURI extracts algorithm, digits, and period", async () => {109 const { parseOtpauthURI } = await import("./totp");110 const uri = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=8&period=60&issuer=Example";111 const params = parseOtpauthURI(uri);112113 expect(params).not.toBeNull();114 expect(params!.algorithm).toBe("SHA256");115 expect(params!.digits).toBe(8);116 expect(params!.period).toBe(60);117 expect(params!.secret).toBeTruthy();118 });119120 121 122 123 it("T-004d: 60-second period — t=0 and t=59 are in the same window", () => {124 const code0 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 60, 0);125 const code59 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 60, 59);126 const code60 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 60, 60);127128 expect(code0).toBe(code59); 129 expect(code0).not.toBe(code60); 130 });131132 133 134 it("T-004e: 8-digit code has exactly 8 numeric characters", () => {135 const code = generateAt(RFC_SECRET_SHA1, "SHA1", 8, 30, 1111111109);136 expect(code).toMatch(/^\d{8}$/);137 expect(code).toBe("07081804");138 });139});