mirror of
https://github.com/nvms/prsm.git
synced 2025-12-16 08:00:53 +00:00
247 lines
13 KiB
TypeScript
247 lines
13 KiB
TypeScript
import * as base32 from "hi-base32";
|
|
import { describe, expect, it } from "vitest";
|
|
import Otp, { InvalidHashFunctionError, InvalidOtpLengthError, InvalidSecretError } from "./index";
|
|
|
|
describe("Otp", () => {
|
|
it("should generate a secret of default strength", () => {
|
|
const secret = Otp.createSecret();
|
|
expect(secret).toBeDefined();
|
|
expect(secret.length).toBe(32);
|
|
});
|
|
|
|
it("should generate a secret of low strength", () => {
|
|
const secret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_LOW);
|
|
expect(secret).toBeDefined();
|
|
expect(secret.length).toBe(16);
|
|
});
|
|
|
|
it("should generate a secret of moderate strength", () => {
|
|
const secret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_MODERATE);
|
|
expect(secret).toBeDefined();
|
|
expect(secret.length).toBe(26);
|
|
});
|
|
|
|
it("should generate a secret of high strength", () => {
|
|
const secret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_HIGH);
|
|
expect(secret).toBeDefined();
|
|
expect(secret.length).toBe(32);
|
|
});
|
|
|
|
it("should generate a valid TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const totp = Otp.generateTotp(secret);
|
|
expect(totp).toBeDefined();
|
|
expect(totp.length).toBe(Otp.OTP_LENGTH_DEFAULT);
|
|
});
|
|
|
|
it("should verify a valid TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const totp = Otp.generateTotp(secret);
|
|
const isValid = Otp.verifyTotp(secret, totp);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should not verify an invalid TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const isValid = Otp.verifyTotp(secret, "123456");
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should generate a valid TOTP with custom length", () => {
|
|
const secret = Otp.createSecret();
|
|
const otpLength = 8;
|
|
const totp = Otp.generateTotp(secret, undefined, otpLength);
|
|
expect(totp).toBeDefined();
|
|
expect(totp.length).toBe(otpLength);
|
|
});
|
|
|
|
it("should verify a valid TOTP with custom length", () => {
|
|
const secret = Otp.createSecret();
|
|
const otpLength = 8;
|
|
const totp = Otp.generateTotp(secret, undefined, otpLength);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, otpLength);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should generate a valid TOTP URI for QR code", () => {
|
|
const secret = Otp.createSecret();
|
|
const uri = Otp.createTotpKeyUriForQrCode("app.example.com", "john.doe@example.org", secret);
|
|
expect(uri).toBeDefined();
|
|
expect(uri).toContain("otpauth://totp/app.example.com:john.doe%40example.org");
|
|
expect(uri).toContain(`secret=${secret}`);
|
|
expect(uri).toContain("issuer=app.example.com");
|
|
});
|
|
|
|
it("should throw an error for invalid secret length", () => {
|
|
expect(() => {
|
|
Otp.generateTotp("shortsecret");
|
|
}).toThrow(InvalidSecretError);
|
|
});
|
|
|
|
it("should throw an error for invalid OTP length", () => {
|
|
const secret = Otp.createSecret();
|
|
expect(() => {
|
|
Otp.generateTotp(secret, undefined, 5);
|
|
}).toThrow(InvalidOtpLengthError);
|
|
expect(() => {
|
|
Otp.generateTotp(secret, undefined, 9);
|
|
}).toThrow(InvalidOtpLengthError);
|
|
});
|
|
|
|
it("should throw an error for invalid hash function", () => {
|
|
const secret = Otp.createSecret();
|
|
expect(() => {
|
|
Otp.generateTotp(secret, undefined, undefined, undefined, undefined, 999);
|
|
}).toThrow(InvalidHashFunctionError);
|
|
});
|
|
|
|
it("should generate and verify TOTP with custom time and interval", () => {
|
|
const secret = Otp.createSecret();
|
|
const customTime = Math.floor(Date.now() / 1000) - 100;
|
|
const customInterval = 60;
|
|
const totp = Otp.generateTotp(secret, customTime, undefined, customInterval);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, customTime, undefined, customInterval);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should handle edge case for time steps", () => {
|
|
const secret = Otp.createSecret();
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
const interval = Otp.INTERVAL_LENGTH_DEFAULT;
|
|
const boundaryTime = currentTime - (currentTime % interval);
|
|
const totp = Otp.generateTotp(secret, boundaryTime);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, boundaryTime);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should not verify an expired TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const pastTime = Math.floor(Date.now() / 1000) - 300; // 5 minutes ago
|
|
const totp = Otp.generateTotp(secret, pastTime);
|
|
const isValid = Otp.verifyTotp(secret, totp);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should not verify a future TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const futureTime = Math.floor(Date.now() / 1000) + 300; // 5 minutes in the future
|
|
const totp = Otp.generateTotp(secret, futureTime);
|
|
const isValid = Otp.verifyTotp(secret, totp);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should verify RFC 6238 test vectors for SHA-1", () => {
|
|
const rfc6238TestKeySha1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // Base32 encoding of '12345678901234567890'
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 3, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 2, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 2, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 2, 2, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 2, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 0, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 3, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 2, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
});
|
|
|
|
it("should verify RFC 6238 test vectors for SHA-256", () => {
|
|
const rfc6238TestKeySha256 = base32.encode("12345678901234567890123456789012"); // 12345678901234567890123456789012
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 3, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 2, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 2, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 2, 2, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 2, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 0, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 3, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 2, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false);
|
|
});
|
|
|
|
it("should verify RFC 6238 test vectors for SHA-512", () => {
|
|
const rfc6238TestKeySha512 = base32.encode("1234567890123456789012345678901234567890123456789012345678901234"); // 1234567890123456789012345678901234567890123456789012345678901234
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 3, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 2, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 2, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 2, 2, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 2, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 0, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 3, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 2, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Otp - 6 Character Length", () => {
|
|
it("should generate a valid 6-character TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const totp = Otp.generateTotp(secret, undefined, 6);
|
|
expect(totp).toBeDefined();
|
|
expect(totp.length).toBe(6);
|
|
});
|
|
|
|
it("should verify a valid 6-character TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const totp = Otp.generateTotp(secret, undefined, 6);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, 6);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should not verify an invalid 6-character TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const isValid = Otp.verifyTotp(secret, "123456", undefined, undefined, undefined, 6);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should generate and verify 6-character TOTP with custom time and interval", () => {
|
|
const secret = Otp.createSecret();
|
|
const customTime = Math.floor(Date.now() / 1000) - 100;
|
|
const customInterval = 60;
|
|
const totp = Otp.generateTotp(secret, customTime, 6, customInterval);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, customTime, 6, customInterval);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should handle edge case for 6-character OTP time steps", () => {
|
|
const secret = Otp.createSecret();
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
const interval = Otp.INTERVAL_LENGTH_DEFAULT;
|
|
const boundaryTime = currentTime - (currentTime % interval);
|
|
const totp = Otp.generateTotp(secret, boundaryTime, 6);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, boundaryTime, 6);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it("should not verify an expired 6-character TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const pastTime = Math.floor(Date.now() / 1000) - 300; // 5 minutes ago
|
|
const totp = Otp.generateTotp(secret, pastTime, 6);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, 6);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should not verify a future 6-character TOTP", () => {
|
|
const secret = Otp.createSecret();
|
|
const futureTime = Math.floor(Date.now() / 1000) + 300; // 5 minutes in the future
|
|
const totp = Otp.generateTotp(secret, futureTime, 6);
|
|
const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, 6);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should verify RFC 6238 test vectors for SHA-1 with 6-character OTP", () => {
|
|
// const rfc6238TestKeySha1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // Base32 encoding of '12345678901234567890'
|
|
const rfc6238TestKeySha1 = base32.encode("12345678901234567890");
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 3, 0, 59 + 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 2, 0, 59 + 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 2, 0, 59 + 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 0, 59 + 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 2, 2, 59, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 0, 59, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 2, 59 - 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 0, 59 - 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 3, 59 - 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true);
|
|
expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 2, 59 - 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false);
|
|
});
|
|
});
|