Files
auth-tools/tests/otp/otp.test.ts
T

135 lines
3.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { utf8ToBytes } from "../../src/crypto/encoding";
import {
hotp,
resynchronizeHotp,
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 });
});
it("resynchronizes with one or two bounded consecutive codes", async () => {
const secret = utf8ToBytes("12345678901234567890");
await expect(
resynchronizeHotp({
firstCode: "338314",
secondCode: "254676",
secret,
counter: 1n,
lookAhead: 10,
}),
).resolves.toEqual({
matchedCounter: 4n,
nextCounter: 6n,
distance: 3,
confidence: "consecutive-codes",
});
await expect(
resynchronizeHotp({
firstCode: "338314",
secondCode: "000000",
secret,
counter: 1n,
lookAhead: 10,
}),
).resolves.toBeNull();
});
});
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);
});
});