import { describe, expect, it } from "vitest"; import { hexToBytes, utf8ToBytes } from "../../src/crypto/encoding"; import { hashOcraPassword, ocra, parseOcraSuite } from "../../src/otp/ocra"; describe("OCRA", () => { it("parses complete suites and rejects reordered inputs", () => { expect( parseOcraSuite("OCRA-1:HOTP-SHA512-8:C-QN08-PSHA1-S064-T1M"), ).toMatchObject({ algorithm: "SHA-512", digits: 8, counter: true, questionFormat: "numeric", passwordAlgorithm: "SHA-1", sessionLength: 64, timeStepSeconds: 60, }); expect(() => parseOcraSuite("OCRA-1:HOTP-SHA1-6:QN08-T1M-PSHA1")).toThrow( /order/iu, ); }); it("matches RFC 6287 one-way challenge vectors", async () => { const secret = utf8ToBytes("12345678901234567890"); const expected = [ "237653", "243178", "653583", "740991", "608993", "388898", "816933", "224598", "750600", "294470", ]; for (let index = 0; index < expected.length; index += 1) { await expect( ocra({ suite: "OCRA-1:HOTP-SHA1-6:QN08", secret, question: String(index).repeat(8), }), ).resolves.toBe(expected[index]); } }); it("matches RFC 6287 counter/PIN and timestamp vectors", async () => { const secret32 = utf8ToBytes("12345678901234567890123456789012"); const pinHash = await hashOcraPassword("1234", "SHA-1"); expect([...pinHash]).toEqual([ ...hexToBytes("7110eda4d09e062aa5e4a390b0a572ac0d2c0220"), ]); await expect( ocra({ suite: "OCRA-1:HOTP-SHA256-8:C-QN08-PSHA1", secret: secret32, counter: 0n, question: "12345678", passwordHash: pinHash, }), ).resolves.toBe("65347737"); const secret64 = utf8ToBytes( "1234567890123456789012345678901234567890123456789012345678901234", ); await expect( ocra({ suite: "OCRA-1:HOTP-SHA512-8:QN08-T1M", secret: secret64, question: "00000000", timeStep: 0x132d0b6n, }), ).resolves.toBe("95209754"); }); });