12345import { describe, expect, it } from "vitest";67import {8 AUTH_TAG_LEN,9 buildHeader,10 CHECKSUM_LEN,11 computeBlake3,12 concatBytes,13 constantTimeEqual,14 extractFields,15 finalisePayload,16 HEADER_LEN,17 MIN_PAYLOAD_LEN,18 NONCE_LEN,19 PAYLOAD_FLAGS,20 PAYLOAD_VERSION,21 remainingBitsForCiphertextLen,22 SALT_LEN,23 type HeaderFields,24} from "./cjkPayload";2526function makeFields(overrides: Partial<HeaderFields> = {}): HeaderFields {27 return {28 entryCount: 1,29 argon2Memory: 65536,30 argon2Time: 3,31 argon2Parallel: 4,32 salt: new Uint8Array(SALT_LEN).fill(0x42),33 remainingBitsValue: 0,34 ...overrides,35 };36}3738describe("buildHeader", () => {39 it("writes the exact byte layout from §2.1", () => {40 const ciphertextLen = 128;41 const rb = remainingBitsForCiphertextLen(ciphertextLen);4243 const header = buildHeader(44 makeFields({ remainingBitsValue: rb, entryCount: 1 })45 );4647 expect(header.length).toBe(HEADER_LEN);48 const view = new DataView(49 header.buffer,50 header.byteOffset,51 header.byteLength52 );5354 expect(header[0]).toBe(PAYLOAD_VERSION);55 expect(view.getUint16(1, true)).toBe(1); 56 expect(header[3]).toBe(PAYLOAD_FLAGS);57 expect(view.getUint32(4, true)).toBe(65536); 58 expect(view.getUint32(8, true)).toBe(3); 59 expect(header[12]).toBe(4); 60 expect(view.getUint16(13, true)).toBe(rb);61 for (let i = 0; i < SALT_LEN; i++) {62 expect(header[15 + i]).toBe(0x42);63 }64 });6566 it("rejects wrongly-sized salts", () => {67 expect(() =>68 buildHeader(makeFields({ salt: new Uint8Array(8) }))69 ).toThrow(/SALT_LEN/);70 });7172 it("rejects entryCounts that overflow uint16", () => {73 expect(() =>74 buildHeader(makeFields({ entryCount: 70000 }))75 ).toThrow(/uint16/);76 });77});7879describe("finalisePayload + extractFields round-trip", () => {80 it("assembles header|nonce|ct|tag|checksum and extracts each field", async () => {81 const ciphertext = new Uint8Array(64);82 crypto.getRandomValues(ciphertext);83 const authTag = new Uint8Array(AUTH_TAG_LEN).fill(0xAA);84 const nonce = new Uint8Array(NONCE_LEN).fill(0x13);85 const rb = remainingBitsForCiphertextLen(ciphertext.length);8687 const header = buildHeader(88 makeFields({ entryCount: 2, remainingBitsValue: rb })89 );9091 const full = await finalisePayload(header, nonce, ciphertext, authTag);9293 94 expect(full.length).toBe(95 HEADER_LEN + NONCE_LEN + ciphertext.length + AUTH_TAG_LEN + CHECKSUM_LEN96 );9798 const extracted = extractFields(full);99100 expect(extracted.entryCount).toBe(2);101 expect(extracted.argon2Memory).toBe(65536);102 expect(extracted.argon2Time).toBe(3);103 expect(extracted.argon2Parallel).toBe(4);104 expect(extracted.remainingBitsValue).toBe(rb);105 expect(extracted.salt).toEqual(new Uint8Array(SALT_LEN).fill(0x42));106 expect(extracted.nonce).toEqual(nonce);107 expect(extracted.ciphertext).toEqual(ciphertext);108 expect(extracted.authTag).toEqual(authTag);109110 111 const recomputed = await computeBlake3(extracted.payloadForChecksum);112 expect(constantTimeEqual(recomputed, extracted.storedChecksum)).toBe(true);113 });114115 it("rejects a truncated payload", () => {116 const tiny = new Uint8Array(MIN_PAYLOAD_LEN - 1);117 expect(() => extractFields(tiny)).toThrow(/PAYLOAD_TOO_SHORT/);118 });119120 it("rejects an unsupported version byte", async () => {121 const ciphertext = new Uint8Array(16);122 const authTag = new Uint8Array(AUTH_TAG_LEN);123 const nonce = new Uint8Array(NONCE_LEN);124 const rb = remainingBitsForCiphertextLen(ciphertext.length);125 const header = buildHeader(makeFields({ remainingBitsValue: rb }));126127 const full = await finalisePayload(header, nonce, ciphertext, authTag);128 full[0] = 0x99; 129 expect(() => extractFields(full)).toThrow(/UNSUPPORTED_VERSION/);130 });131132 it("rejects unsupported flags", async () => {133 const ciphertext = new Uint8Array(16);134 const authTag = new Uint8Array(AUTH_TAG_LEN);135 const nonce = new Uint8Array(NONCE_LEN);136 const rb = remainingBitsForCiphertextLen(ciphertext.length);137 const header = buildHeader(makeFields({ remainingBitsValue: rb }));138139 const full = await finalisePayload(header, nonce, ciphertext, authTag);140 full[3] = 0x05; 141 expect(() => extractFields(full)).toThrow(/UNSUPPORTED_FLAGS/);142 });143});144145describe("constantTimeEqual", () => {146 it("returns true for identical buffers", () => {147 const a = new Uint8Array([1, 2, 3, 4]);148 const b = new Uint8Array([1, 2, 3, 4]);149 expect(constantTimeEqual(a, b)).toBe(true);150 });151152 it("returns false when any byte differs", () => {153 const a = new Uint8Array([1, 2, 3, 4]);154 const b = new Uint8Array([1, 2, 3, 5]);155 expect(constantTimeEqual(a, b)).toBe(false);156 });157158 it("returns false for different lengths", () => {159 expect(160 constantTimeEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3]))161 ).toBe(false);162 });163});164165describe("concatBytes", () => {166 it("concatenates in order", () => {167 const out = concatBytes([168 new Uint8Array([1, 2]),169 new Uint8Array([3]),170 new Uint8Array([4, 5, 6]),171 ]);172 expect(out).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6]));173 });174});