import { describe, expect, it } from 'vitest' import type { AeadAlg } from 'relay-contracts' import { aeadOpen, aeadSeal, importAeadKey, nonceLength, tagLength } from '../src/aead.js' import { AeadOpenError, E2EError } from '../src/errors.js' import { bytesToHex, hexToBytes } from './helpers.js' import aeadVectors from './vectors/aead.json' with { type: 'json' } const ALGS: AeadAlg[] = ['aes-256-gcm', 'xchacha20-poly1305'] describe('T2 aead', () => { it('nonceLength / tagLength per alg (§4.4)', () => { expect(nonceLength('aes-256-gcm')).toBe(12) expect(nonceLength('xchacha20-poly1305')).toBe(24) expect(tagLength('aes-256-gcm')).toBe(16) }) it('KAT: each frozen vector seals to the expected ciphertext+tag and opens back', () => { for (const v of aeadVectors) { const key = importAeadKey(hexToBytes(v.key), v.alg as AeadAlg, 'c2h') const sealed = aeadSeal( key, hexToBytes(v.nonce), hexToBytes(v.plaintext), hexToBytes(v.aad), ) expect(bytesToHex(sealed.ciphertext)).toBe(v.ciphertext) expect(bytesToHex(sealed.tag)).toBe(v.tag) const opened = aeadOpen(key, hexToBytes(v.nonce), sealed.ciphertext, sealed.tag, hexToBytes(v.aad)) expect(bytesToHex(opened)).toBe(v.plaintext) } }) it.each(ALGS)('round-trips empty and 1 MiB plaintext (%s)', (alg) => { const key = importAeadKey(new Uint8Array(32).fill(9), alg, 'c2h') const nonce = new Uint8Array(nonceLength(alg)).fill(1) const aad = new Uint8Array([7, 7]) for (const pt of [new Uint8Array(0), new Uint8Array(1024 * 1024).fill(0xa5)]) { const { ciphertext, tag } = aeadSeal(key, nonce, pt, aad) const opened = aeadOpen(key, nonce, ciphertext, tag, aad) expect(opened).toEqual(pt) } }) it.each(ALGS)('INV13 substrate: cipher-bit / aad-byte / wrong-key tamper → AeadOpenError (%s)', (alg) => { const key = importAeadKey(new Uint8Array(32).fill(3), alg, 'c2h') const wrong = importAeadKey(new Uint8Array(32).fill(4), alg, 'c2h') const nonce = new Uint8Array(nonceLength(alg)).fill(2) const aad = new Uint8Array([1, 2, 3]) const { ciphertext, tag } = aeadSeal(key, nonce, new TextEncoder().encode('secret'), aad) const flippedCt = ciphertext.slice() flippedCt[0]! ^= 0x01 expect(() => aeadOpen(key, nonce, flippedCt, tag, aad)).toThrow(AeadOpenError) const flippedAad = aad.slice() flippedAad[0]! ^= 0x01 expect(() => aeadOpen(key, nonce, ciphertext, tag, flippedAad)).toThrow(AeadOpenError) expect(() => aeadOpen(wrong, nonce, ciphertext, tag, aad)).toThrow(AeadOpenError) }) it('nonce width mismatch → typed error, not silent truncation', () => { const key = importAeadKey(new Uint8Array(32).fill(1), 'xchacha20-poly1305', 'c2h') expect(() => aeadSeal(key, new Uint8Array(12), new Uint8Array(1), new Uint8Array(0))).toThrow( E2EError, ) }) it('importAeadKey rejects a non-32-byte key', () => { expect(() => importAeadKey(new Uint8Array(16), 'aes-256-gcm', '')).toThrow(E2EError) }) })