import { ValidationError } from '@shade/core'; export const STREAM_NONCE_BYTES = 12; /** Maximum chunk seq value (u64 max). Hard-spec'd hard limit. */ export const MAX_SEQ = 0xffff_ffff_ffff_ffffn; /** * Construct the deterministic AES-GCM nonce for a stream chunk. * * nonce[0..4] = u32_be(laneId) * nonce[4..12] = u64_be(seq) * * Per (laneId, seq) is unique — combined with the lane-specific key, this * guarantees AES-GCM nonce-uniqueness even across multiple parallel lanes. */ export function buildChunkNonce(laneId: number, seq: number | bigint): Uint8Array { if (!Number.isInteger(laneId) || laneId < 0 || laneId > 0xffff_ffff) { throw new ValidationError(`laneId must fit in u32: ${laneId}`, 'laneId'); } const seqBig = typeof seq === 'bigint' ? seq : BigInt(seq); if (seqBig < 0n || seqBig > MAX_SEQ) { throw new ValidationError(`seq must fit in u64 (>= 0): ${seq}`, 'seq'); } const out = new Uint8Array(STREAM_NONCE_BYTES); const view = new DataView(out.buffer); view.setUint32(0, laneId, false); view.setBigUint64(4, seqBig, false); return out; } /** * Build the AAD bound to a stream chunk. Computed implicitly on both sides * from the chunk header (never transmitted as-is). Tampering with any header * field invalidates the AEAD tag. * * aad = streamId(16) || u32_be(laneId) || u64_be(seq) || u8(isLast) */ export function buildChunkAad( streamId: Uint8Array, laneId: number, seq: number | bigint, isLast: boolean, ): Uint8Array { if (streamId.length !== 16) { throw new ValidationError('streamId must be 16 bytes', 'streamId'); } if (!Number.isInteger(laneId) || laneId < 0 || laneId > 0xffff_ffff) { throw new ValidationError(`laneId must fit in u32: ${laneId}`, 'laneId'); } const seqBig = typeof seq === 'bigint' ? seq : BigInt(seq); if (seqBig < 0n || seqBig > MAX_SEQ) { throw new ValidationError(`seq must fit in u64 (>= 0): ${seq}`, 'seq'); } const out = new Uint8Array(16 + 4 + 8 + 1); out.set(streamId, 0); const view = new DataView(out.buffer); view.setUint32(16, laneId, false); view.setBigUint64(20, seqBig, false); out[28] = isLast ? 0x01 : 0x00; return out; }