import { describe, expect, it } from 'vitest' import { unwrapAeadKey } from '../src/aead-key.js' import { deriveMaster, deriveSessionKeys } from '../src/hkdf.js' import { bytesToHex, hexToBytes } from './helpers.js' import hkdfVector from './vectors/hkdf.json' with { type: 'json' } const shared = hexToBytes(hkdfVector.sharedSecret) const cn = hexToBytes(hkdfVector.clientNonce) const hn = hexToBytes(hkdfVector.hostNonce) describe('T6 hkdf direction-split', () => { it('KAT: reproduces the frozen master + both direction subkeys', async () => { expect(bytesToHex(deriveMaster(shared, cn, hn))).toBe(hkdfVector.master) const dk = await deriveSessionKeys(shared, cn, hn, 'aes-256-gcm') expect(bytesToHex(unwrapAeadKey(dk.c2h).raw)).toBe(hkdfVector.c2h) expect(bytesToHex(unwrapAeadKey(dk.h2c).raw)).toBe(hkdfVector.h2c) }) it('deterministic: same inputs → same keys', async () => { const a = await deriveSessionKeys(shared, cn, hn, 'aes-256-gcm') const b = await deriveSessionKeys(shared, cn, hn, 'aes-256-gcm') expect(bytesToHex(unwrapAeadKey(a.c2h).raw)).toBe(bytesToHex(unwrapAeadKey(b.c2h).raw)) }) it('changing EITHER nonce changes the keys (salt = both nonces)', async () => { const base = await deriveSessionKeys(shared, cn, hn, 'aes-256-gcm') const altC = await deriveSessionKeys(shared, hexToBytes(hkdfVector.hostNonce), hn, 'aes-256-gcm') const altH = await deriveSessionKeys(shared, cn, hexToBytes(hkdfVector.clientNonce), 'aes-256-gcm') expect(bytesToHex(unwrapAeadKey(base.c2h).raw)).not.toBe(bytesToHex(unwrapAeadKey(altC.c2h).raw)) expect(bytesToHex(unwrapAeadKey(base.c2h).raw)).not.toBe(bytesToHex(unwrapAeadKey(altH.c2h).raw)) }) it('SECURITY direction disjointness: c2h !== h2c, each 32 bytes, neither equals the master', async () => { const dk = await deriveSessionKeys(shared, cn, hn, 'aes-256-gcm') const c2h = unwrapAeadKey(dk.c2h).raw const h2c = unwrapAeadKey(dk.h2c).raw const master = deriveMaster(shared, cn, hn) expect(c2h.length).toBe(32) expect(h2c.length).toBe(32) expect(bytesToHex(c2h)).not.toBe(bytesToHex(h2c)) expect(bytesToHex(c2h)).not.toBe(bytesToHex(master)) expect(bytesToHex(h2c)).not.toBe(bytesToHex(master)) }) it('subkeys carry their direction label so seal binds direction into aad', async () => { const dk = await deriveSessionKeys(shared, cn, hn, 'xchacha20-poly1305') expect(unwrapAeadKey(dk.c2h).aadLabel).toBe('c2h') expect(unwrapAeadKey(dk.h2c).aadLabel).toBe('h2c') expect(unwrapAeadKey(dk.c2h).alg).toBe('xchacha20-poly1305') }) })