12345678910import { describe, expect, it } from "vitest";11import { TOTP } from "otpauth";121314151617const RFC_SECRET_SHA1 = "12345678901234567890";18const RFC_SECRET_SHA256 = "12345678901234567890123456789012";19const RFC_SECRET_SHA512 =20 "1234567890123456789012345678901234567890123456789012345678901234";212223function generateAt(secret: string, algorithm: string, digits: number, period: number, t: number): string {24 const totp = new TOTP({ secret, algorithm, digits, period });25 return totp.generate({ timestamp: t * 1000 });26}2728describe("T-004: TOTP RFC 6238 known-answer vectors", () => {29 30 31 32 33 34 35 describe("T-004a: RFC 6238 SHA-1 vectors (8 digits, 30s period)", () => {36 const sha1Vectors: Array<{ t: number; expected: string }> = [37 { t: 59, expected: "94287082" },38 { t: 1111111109, expected: "07081804" },39 { t: 1111111111, expected: "14050471" },40 { t: 1234567890, expected: "89005924" },41 { t: 2000000000, expected: "69279037" },42 { t: 20000000000, expected: "65353130" },43 ];4445 for (const { t, expected } of sha1Vectors) {46 it(`t=${t} → ${expected}`, () => {47 const code = generateAt(RFC_SECRET_SHA1, "SHA1", 8, 30, t);48 expect(code).toBe(expected);49 });50 }51 });5253 54 55 describe("T-004a: RFC 6238 SHA-256 vectors (8 digits, 30s period)", () => {56 const sha256Vectors: Array<{ t: number; expected: string }> = [57 { t: 59, expected: "46119246" },58 { t: 1111111109, expected: "68084774" },59 { t: 1111111111, expected: "67062674" },60 { t: 1234567890, expected: "91819424" },61 { t: 2000000000, expected: "90698825" },62 { t: 20000000000, expected: "77737706" },63 ];6465 for (const { t, expected } of sha256Vectors) {66 it(`t=${t} → ${expected}`, () => {67 const code = generateAt(RFC_SECRET_SHA256, "SHA256", 8, 30, t);68 expect(code).toBe(expected);69 });70 }71 });7273 74 describe("T-004a: RFC 6238 SHA-512 vectors (8 digits, 30s period)", () => {75 const sha512Vectors: Array<{ t: number; expected: string }> = [76 { t: 59, expected: "90693936" },77 { t: 1111111109, expected: "25091201" },78 { t: 1111111111, expected: "99943326" },79 { t: 1234567890, expected: "93441116" },80 { t: 2000000000, expected: "38618901" },81 { t: 20000000000, expected: "47863826" },82 ];8384 for (const { t, expected } of sha512Vectors) {85 it(`t=${t} → ${expected}`, () => {86 const code = generateAt(RFC_SECRET_SHA512, "SHA512", 8, 30, t);87 expect(code).toBe(expected);88 });89 }90 });9192 93 94 95 96 97 it("T-004b: period-boundary roll — t=29 and t=30 produce different codes", () => {98 const code29 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 30, 29);99 const code30 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 30, 30);100 expect(code29).not.toBe(code30);101 });102103 104 105 106 it("T-004c: parseOtpauthURI extracts algorithm, digits, and period", async () => {107 const { parseOtpauthURI } = await import("../../crypto/totp");108 const uri = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=8&period=60&issuer=Example";109 const params = parseOtpauthURI(uri);110111 expect(params).not.toBeNull();112 expect(params!.algorithm).toBe("SHA256");113 expect(params!.digits).toBe(8);114 expect(params!.period).toBe(60);115 expect(params!.secret).toBeTruthy();116 });117118 119 120 121 it("T-004d: 60-second period — t=0 and t=59 are in the same window", () => {122 const code0 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 60, 0);123 const code59 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 60, 59);124 const code60 = generateAt(RFC_SECRET_SHA1, "SHA1", 6, 60, 60);125126 expect(code0).toBe(code59); 127 expect(code0).not.toBe(code60); 128 });129130 131 132 it("T-004e: 8-digit code has exactly 8 numeric characters", () => {133 const code = generateAt(RFC_SECRET_SHA1, "SHA1", 8, 30, 1111111109);134 expect(code).toMatch(/^\d{8}$/);135 expect(code).toBe("07081804");136 });137});