Skip to main content

logicaffeine_system/
aead.rs

1//! ChaCha20-Poly1305 AEAD (RFC 8439) — the symmetric seal that closes the post-quantum channel:
2//! an ML-KEM-768 handshake establishes the shared secret, HKDF derives this 32-byte key, and every
3//! wire message is sealed here. ChaCha20 is a pure `Word32` ARX cipher (the same primitive the
4//! `assets/std/crypto.lg` `chacha20Block` ships in Logos); Poly1305 is the one-time authenticator in
5//! constant-time 5×26-bit limbs (no bignum). Verified against the RFC 8439 §2.4.2 / §2.5.2 / §2.8.2
6//! known-answer vectors. The tag compare is constant-time.
7
8// ── ChaCha20 (RFC 8439 §2.3–2.4) ───────────────────────────────────────────────────────────────
9
10#[inline]
11fn qr(s: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
12    s[a] = s[a].wrapping_add(s[b]);
13    s[d] = (s[d] ^ s[a]).rotate_left(16);
14    s[c] = s[c].wrapping_add(s[d]);
15    s[b] = (s[b] ^ s[c]).rotate_left(12);
16    s[a] = s[a].wrapping_add(s[b]);
17    s[d] = (s[d] ^ s[a]).rotate_left(8);
18    s[c] = s[c].wrapping_add(s[d]);
19    s[b] = (s[b] ^ s[c]).rotate_left(7);
20}
21
22fn chacha20_block(key: &[u32; 8], counter: u32, nonce: &[u32; 3]) -> [u32; 16] {
23    let mut state = [
24        0x6170_7865, 0x3320_646e, 0x7962_2d32, 0x6b20_6574, key[0], key[1], key[2], key[3], key[4],
25        key[5], key[6], key[7], counter, nonce[0], nonce[1], nonce[2],
26    ];
27    let mut w = state;
28    for _ in 0..10 {
29        qr(&mut w, 0, 4, 8, 12);
30        qr(&mut w, 1, 5, 9, 13);
31        qr(&mut w, 2, 6, 10, 14);
32        qr(&mut w, 3, 7, 11, 15);
33        qr(&mut w, 0, 5, 10, 15);
34        qr(&mut w, 1, 6, 11, 12);
35        qr(&mut w, 2, 7, 8, 13);
36        qr(&mut w, 3, 4, 9, 14);
37    }
38    for i in 0..16 {
39        state[i] = w[i].wrapping_add(state[i]);
40    }
41    state
42}
43
44fn le_u32x8(b: &[u8; 32]) -> [u32; 8] {
45    std::array::from_fn(|i| u32::from_le_bytes(b[i * 4..i * 4 + 4].try_into().unwrap()))
46}
47fn le_u32x3(b: &[u8; 12]) -> [u32; 3] {
48    std::array::from_fn(|i| u32::from_le_bytes(b[i * 4..i * 4 + 4].try_into().unwrap()))
49}
50
51/// ChaCha20 keystream XOR (RFC 8439 §2.4): `data ⊕ ChaCha20(key, counter‖nonce)`. AVX2 (8 blocks at
52/// a time) when available, else scalar — bit-identical.
53pub fn chacha20_xor(key: &[u8; 32], nonce: &[u8; 12], counter: u32, data: &[u8]) -> Vec<u8> {
54    #[cfg(target_arch = "x86_64")]
55    {
56        // The 8-block AVX2 path computes 512 B of keystream at once, so it only pays off past a few
57        // blocks; below that the scalar single-block path avoids the wasted work.
58        if data.len() >= 256 && std::is_x86_feature_detected!("avx2") {
59            return unsafe { chacha20_xor_avx2(&le_u32x8(key), &le_u32x3(nonce), counter, data) };
60        }
61    }
62    chacha20_xor_scalar(key, nonce, counter, data)
63}
64
65fn chacha20_xor_scalar(key: &[u8; 32], nonce: &[u8; 12], counter: u32, data: &[u8]) -> Vec<u8> {
66    let kw = le_u32x8(key);
67    let nw = le_u32x3(nonce);
68    let mut out = Vec::with_capacity(data.len());
69    for (bi, chunk) in data.chunks(64).enumerate() {
70        let block = chacha20_block(&kw, counter.wrapping_add(bi as u32), &nw);
71        let mut ks = [0u8; 64];
72        for i in 0..16 {
73            ks[i * 4..i * 4 + 4].copy_from_slice(&block[i].to_le_bytes());
74        }
75        for (j, &b) in chunk.iter().enumerate() {
76            out.push(b ^ ks[j]);
77        }
78    }
79    out
80}
81
82/// AVX2 ChaCha20: 8 blocks computed in parallel (each `__m256i` lane is one block's word at a fixed
83/// index), the keystream serialized and XORed. The `<512`-byte tail falls back to the scalar block.
84#[cfg(target_arch = "x86_64")]
85#[target_feature(enable = "avx2")]
86unsafe fn chacha20_xor_avx2(key: &[u32; 8], nonce: &[u32; 3], counter: u32, data: &[u8]) -> Vec<u8> {
87    use std::arch::x86_64::*;
88    let rot16 = _mm256_setr_epi8(
89        2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9, 14, 15, 12, 13, 2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9,
90        14, 15, 12, 13,
91    );
92    let rot8 = _mm256_setr_epi8(
93        3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14, 3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10,
94        15, 12, 13, 14,
95    );
96    #[inline(always)]
97    unsafe fn rotl12(x: std::arch::x86_64::__m256i) -> std::arch::x86_64::__m256i {
98        use std::arch::x86_64::*;
99        _mm256_or_si256(_mm256_slli_epi32::<12>(x), _mm256_srli_epi32::<20>(x))
100    }
101    #[inline(always)]
102    unsafe fn rotl7(x: std::arch::x86_64::__m256i) -> std::arch::x86_64::__m256i {
103        use std::arch::x86_64::*;
104        _mm256_or_si256(_mm256_slli_epi32::<7>(x), _mm256_srli_epi32::<25>(x))
105    }
106
107    let mut out = Vec::with_capacity(data.len());
108    let lane_ctr = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
109    let mut off = 0usize;
110    let mut base = counter;
111    while off < data.len() {
112        let init: [__m256i; 16] = [
113            _mm256_set1_epi32(0x6170_7865u32 as i32),
114            _mm256_set1_epi32(0x3320_646eu32 as i32),
115            _mm256_set1_epi32(0x7962_2d32u32 as i32),
116            _mm256_set1_epi32(0x6b20_6574u32 as i32),
117            _mm256_set1_epi32(key[0] as i32),
118            _mm256_set1_epi32(key[1] as i32),
119            _mm256_set1_epi32(key[2] as i32),
120            _mm256_set1_epi32(key[3] as i32),
121            _mm256_set1_epi32(key[4] as i32),
122            _mm256_set1_epi32(key[5] as i32),
123            _mm256_set1_epi32(key[6] as i32),
124            _mm256_set1_epi32(key[7] as i32),
125            _mm256_add_epi32(_mm256_set1_epi32(base as i32), lane_ctr),
126            _mm256_set1_epi32(nonce[0] as i32),
127            _mm256_set1_epi32(nonce[1] as i32),
128            _mm256_set1_epi32(nonce[2] as i32),
129        ];
130        let mut v = init;
131        macro_rules! qr {
132            ($a:tt, $b:tt, $c:tt, $d:tt) => {
133                v[$a] = _mm256_add_epi32(v[$a], v[$b]);
134                v[$d] = _mm256_shuffle_epi8(_mm256_xor_si256(v[$d], v[$a]), rot16);
135                v[$c] = _mm256_add_epi32(v[$c], v[$d]);
136                v[$b] = rotl12(_mm256_xor_si256(v[$b], v[$c]));
137                v[$a] = _mm256_add_epi32(v[$a], v[$b]);
138                v[$d] = _mm256_shuffle_epi8(_mm256_xor_si256(v[$d], v[$a]), rot8);
139                v[$c] = _mm256_add_epi32(v[$c], v[$d]);
140                v[$b] = rotl7(_mm256_xor_si256(v[$b], v[$c]));
141            };
142        }
143        for _ in 0..10 {
144            qr!(0, 4, 8, 12);
145            qr!(1, 5, 9, 13);
146            qr!(2, 6, 10, 14);
147            qr!(3, 7, 11, 15);
148            qr!(0, 5, 10, 15);
149            qr!(1, 6, 11, 12);
150            qr!(2, 7, 8, 13);
151            qr!(3, 4, 9, 14);
152        }
153        let mut words = [[0u32; 8]; 16];
154        for j in 0..16 {
155            let sum = _mm256_add_epi32(v[j], init[j]);
156            _mm256_storeu_si256(words[j].as_mut_ptr() as *mut __m256i, sum);
157        }
158        let mut ks = [0u8; 512];
159        for blk in 0..8 {
160            for j in 0..16 {
161                ks[blk * 64 + j * 4..blk * 64 + j * 4 + 4]
162                    .copy_from_slice(&words[j][blk].to_le_bytes());
163            }
164        }
165        let n = (data.len() - off).min(512);
166        let dst_start = out.len();
167        out.resize(dst_start + n, 0);
168        let dst = out.as_mut_ptr().add(dst_start);
169        let src = data.as_ptr().add(off);
170        let mut i = 0;
171        while i + 32 <= n {
172            let a = _mm256_loadu_si256(src.add(i) as *const __m256i);
173            let b = _mm256_loadu_si256(ks.as_ptr().add(i) as *const __m256i);
174            _mm256_storeu_si256(dst.add(i) as *mut __m256i, _mm256_xor_si256(a, b));
175            i += 32;
176        }
177        while i < n {
178            *dst.add(i) = *src.add(i) ^ ks[i];
179            i += 1;
180        }
181        off += 512;
182        base = base.wrapping_add(8);
183    }
184    out
185}
186
187// ── Poly1305 (RFC 8439 §2.5), 5×26-bit limbs, constant-time ────────────────────────────────────
188
189const MASK26: u32 = 0x3ff_ffff;
190
191/// Clamp `r` into 5×26-bit limbs.
192fn poly1305_clamp(key: &[u8; 32]) -> [u32; 5] {
193    let t0 = u32::from_le_bytes([key[0], key[1], key[2], key[3]]);
194    let t1 = u32::from_le_bytes([key[4], key[5], key[6], key[7]]);
195    let t2 = u32::from_le_bytes([key[8], key[9], key[10], key[11]]);
196    let t3 = u32::from_le_bytes([key[12], key[13], key[14], key[15]]);
197    [
198        t0 & 0x3ff_ffff,
199        ((t0 >> 26) | (t1 << 6)) & 0x3ff_ff03,
200        ((t1 >> 20) | (t2 << 12)) & 0x3ff_c0ff,
201        ((t2 >> 14) | (t3 << 18)) & 0x3f0_3fff,
202        (t3 >> 8) & 0x00f_ffff,
203    ]
204}
205
206/// A 16-byte block's 5×26-bit limbs; `hibit = 1` adds the 2¹²⁸ marker (a full block — for a partial
207/// block the caller writes the appended `1` byte inside `b` and passes `hibit = 0`).
208fn poly1305_block(b: &[u8; 16], hibit: u32) -> [u32; 5] {
209    let m0 = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
210    let m1 = u32::from_le_bytes([b[4], b[5], b[6], b[7]]);
211    let m2 = u32::from_le_bytes([b[8], b[9], b[10], b[11]]);
212    let m3 = u32::from_le_bytes([b[12], b[13], b[14], b[15]]);
213    [
214        m0 & MASK26,
215        ((m0 >> 26) | (m1 << 6)) & MASK26,
216        ((m1 >> 20) | (m2 << 12)) & MASK26,
217        ((m2 >> 14) | (m3 << 18)) & MASK26,
218        (m3 >> 8) | (hibit << 24),
219    ]
220}
221
222/// Carry-reduce 5 wide limbs (each ≤ ~2⁶⁰, e.g. the sum of four 130-bit products) into 5×26-bit
223/// limbs mod (2¹³⁰−5).
224fn poly1305_reduce(d: [u64; 5]) -> [u32; 5] {
225    let m = MASK26 as u64;
226    let mut c = d[0] >> 26;
227    let h0 = (d[0] & m) as u32;
228    let d1 = d[1] + c;
229    c = d1 >> 26;
230    let h1 = (d1 & m) as u32;
231    let d2 = d[2] + c;
232    c = d2 >> 26;
233    let h2 = (d2 & m) as u32;
234    let d3 = d[3] + c;
235    c = d3 >> 26;
236    let h3 = (d3 & m) as u32;
237    let d4 = d[4] + c;
238    c = d4 >> 26;
239    let h4 = (d4 & m) as u32;
240    // Top carry `c` (up to ~2³⁴) wraps via ·5 into limb 0 — it can span more than one limb.
241    let h0w = h0 as u64 + c * 5;
242    let mut h = [(h0w & m) as u32, h1 + (h0w >> 26) as u32, h2, h3, h4];
243    let c1 = h[1] >> 26;
244    h[1] &= MASK26;
245    h[2] += c1;
246    h
247}
248
249/// `a · b mod (2¹³⁰−5)` over 5×26-bit limbs (inputs ≤ ~2²⁷), fully reduced.
250fn poly1305_mul(a: &[u32; 5], b: &[u32; 5]) -> [u32; 5] {
251    let (a0, a1, a2, a3, a4) = (a[0] as u64, a[1] as u64, a[2] as u64, a[3] as u64, a[4] as u64);
252    let (b0, b1, b2, b3, b4) = (b[0] as u64, b[1] as u64, b[2] as u64, b[3] as u64, b[4] as u64);
253    let (s1, s2, s3, s4) = (b1 * 5, b2 * 5, b3 * 5, b4 * 5);
254    poly1305_reduce([
255        a0 * b0 + a1 * s4 + a2 * s3 + a3 * s2 + a4 * s1,
256        a0 * b1 + a1 * b0 + a2 * s4 + a3 * s3 + a4 * s2,
257        a0 * b2 + a1 * b1 + a2 * b0 + a3 * s4 + a4 * s3,
258        a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0 + a4 * s4,
259        a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0,
260    ])
261}
262
263/// Finalize: full reduction mod p, then `+ s mod 2¹²⁸` → the 16-byte tag.
264fn poly1305_finalize(h: [u32; 5], key: &[u8; 32]) -> [u8; 16] {
265    let (mut h0, mut h1, mut h2, mut h3, mut h4) = (h[0], h[1], h[2], h[3], h[4]);
266    let mut c;
267    c = h1 >> 26;
268    h1 &= 0x3ff_ffff;
269    h2 += c;
270    c = h2 >> 26;
271    h2 &= 0x3ff_ffff;
272    h3 += c;
273    c = h3 >> 26;
274    h3 &= 0x3ff_ffff;
275    h4 += c;
276    c = h4 >> 26;
277    h4 &= 0x3ff_ffff;
278    h0 += c * 5;
279    c = h0 >> 26;
280    h0 &= 0x3ff_ffff;
281    h1 += c;
282    let mut g0 = h0.wrapping_add(5);
283    c = g0 >> 26;
284    g0 &= 0x3ff_ffff;
285    let mut g1 = h1.wrapping_add(c);
286    c = g1 >> 26;
287    g1 &= 0x3ff_ffff;
288    let mut g2 = h2.wrapping_add(c);
289    c = g2 >> 26;
290    g2 &= 0x3ff_ffff;
291    let mut g3 = h3.wrapping_add(c);
292    c = g3 >> 26;
293    g3 &= 0x3ff_ffff;
294    let g4 = h4.wrapping_add(c).wrapping_sub(1 << 26);
295    let mask = (g4 >> 31).wrapping_sub(1);
296    let nmask = !mask;
297    h0 = (h0 & nmask) | (g0 & mask);
298    h1 = (h1 & nmask) | (g1 & mask);
299    h2 = (h2 & nmask) | (g2 & mask);
300    h3 = (h3 & nmask) | (g3 & mask);
301    h4 = (h4 & nmask) | (g4 & mask);
302    let f0 = (h0 | (h1 << 26)) as u64;
303    let f1 = ((h1 >> 6) | (h2 << 20)) as u64;
304    let f2 = ((h2 >> 12) | (h3 << 14)) as u64;
305    let f3 = ((h3 >> 18) | (h4 << 8)) as u64;
306    let s0 = u32::from_le_bytes([key[16], key[17], key[18], key[19]]) as u64;
307    let s1 = u32::from_le_bytes([key[20], key[21], key[22], key[23]]) as u64;
308    let s2 = u32::from_le_bytes([key[24], key[25], key[26], key[27]]) as u64;
309    let s3 = u32::from_le_bytes([key[28], key[29], key[30], key[31]]) as u64;
310    let mut tag = [0u8; 16];
311    let mut f = f0 + s0;
312    tag[0..4].copy_from_slice(&(f as u32).to_le_bytes());
313    f = f1 + s1 + (f >> 32);
314    tag[4..8].copy_from_slice(&(f as u32).to_le_bytes());
315    f = f2 + s2 + (f >> 32);
316    tag[8..12].copy_from_slice(&(f as u32).to_le_bytes());
317    f = f3 + s3 + (f >> 32);
318    tag[12..16].copy_from_slice(&(f as u32).to_le_bytes());
319    tag
320}
321
322/// Process one block scalar: `h ← (h + block) · r`.
323#[inline]
324fn poly1305_absorb(h: &mut [u32; 5], msg: &[u8], i: usize, r: &[u32; 5]) {
325    let n = (msg.len() - i).min(16);
326    let mut b = [0u8; 16];
327    b[..n].copy_from_slice(&msg[i..i + n]);
328    let hibit = if n == 16 {
329        1
330    } else {
331        b[n] = 1;
332        0
333    };
334    let m = poly1305_block(&b, hibit);
335    let sum = [h[0] + m[0], h[1] + m[1], h[2] + m[2], h[3] + m[3], h[4] + m[4]];
336    *h = poly1305_mul(&sum, r);
337}
338
339/// Poly1305 one-time authenticator: `tag = ((Σ blockᵢ·r^(n−i)) mod (2¹³⁰−5)) + s mod 2¹²⁸`. AVX2
340/// (4 blocks/group via precomputed r¹..r⁴) for long messages, else scalar — bit-identical.
341pub fn poly1305(key: &[u8; 32], msg: &[u8]) -> [u8; 16] {
342    #[cfg(target_arch = "x86_64")]
343    {
344        if msg.len() >= 256 && std::is_x86_feature_detected!("avx2") {
345            return unsafe { poly1305_avx2(key, msg) };
346        }
347    }
348    poly1305_scalar(key, msg)
349}
350
351fn poly1305_scalar(key: &[u8; 32], msg: &[u8]) -> [u8; 16] {
352    let r = poly1305_clamp(key);
353    let mut h = [0u32; 5];
354    let mut i = 0;
355    while i < msg.len() {
356        poly1305_absorb(&mut h, msg, i, &r);
357        i += 16;
358    }
359    poly1305_finalize(h, key)
360}
361
362/// Horizontal sum of the four u64 lanes.
363#[cfg(target_arch = "x86_64")]
364#[target_feature(enable = "avx2")]
365#[inline]
366unsafe fn poly1305_hsum(v: std::arch::x86_64::__m256i) -> u64 {
367    use std::arch::x86_64::*;
368    let lo = _mm256_castsi256_si128(v);
369    let hi = _mm256_extracti128_si256::<1>(v);
370    let s = _mm_add_epi64(lo, hi);
371    let s2 = _mm_add_epi64(s, _mm_unpackhi_epi64(s, s));
372    _mm_cvtsi128_si64(s2) as u64
373}
374
375/// Per-limb multiplier vectors `[r⁴, r³, r², r¹]` (and `5×` for the 2¹³⁰ wrap) at even 32-bit lanes.
376#[cfg(target_arch = "x86_64")]
377#[target_feature(enable = "avx2")]
378unsafe fn poly1305_vectors(
379    r: &[u32; 5],
380) -> ([std::arch::x86_64::__m256i; 5], [std::arch::x86_64::__m256i; 5]) {
381    use std::arch::x86_64::*;
382    let r2 = poly1305_mul(r, r);
383    let r3 = poly1305_mul(&r2, r);
384    let r4 = poly1305_mul(&r2, &r2);
385    let mut vb = [_mm256_setzero_si256(); 5];
386    let mut vs = [_mm256_setzero_si256(); 5];
387    for l in 0..5 {
388        vb[l] = _mm256_set_epi32(0, r[l] as i32, 0, r2[l] as i32, 0, r3[l] as i32, 0, r4[l] as i32);
389        vs[l] = _mm256_set_epi32(
390            0, (r[l] * 5) as i32, 0, (r2[l] * 5) as i32, 0, (r3[l] * 5) as i32, 0, (r4[l] * 5) as i32,
391        );
392    }
393    (vb, vs)
394}
395
396/// Absorb the full 4-block groups of `data` into `h` (four products/group via `_mm256_mul_epu32`,
397/// one per lane, summed), each block carrying the 2¹²⁸ marker. Returns bytes consumed (`groups·64`).
398#[cfg(target_arch = "x86_64")]
399#[target_feature(enable = "avx2")]
400unsafe fn poly1305_groups(
401    h: &mut [u32; 5],
402    data: &[u8],
403    vb: &[std::arch::x86_64::__m256i; 5],
404    vs: &[std::arch::x86_64::__m256i; 5],
405) -> usize {
406    use std::arch::x86_64::*;
407    let groups = (data.len() / 16) / 4;
408    for g in 0..groups {
409        let base = g * 64;
410        let mut t = [[0u32; 5]; 4];
411        for k in 0..4 {
412            let b: &[u8; 16] = data[base + k * 16..base + k * 16 + 16].try_into().unwrap();
413            t[k] = poly1305_block(b, 1);
414        }
415        for l in 0..5 {
416            t[0][l] += h[l];
417        }
418        let mut va = [_mm256_setzero_si256(); 5];
419        for l in 0..5 {
420            va[l] = _mm256_set_epi32(
421                0, t[3][l] as i32, 0, t[2][l] as i32, 0, t[1][l] as i32, 0, t[0][l] as i32,
422            );
423        }
424        macro_rules! mul {
425            ($x:expr, $y:expr) => {
426                _mm256_mul_epu32($x, $y)
427            };
428        }
429        macro_rules! add5 {
430            ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr) => {
431                _mm256_add_epi64(
432                    _mm256_add_epi64(_mm256_add_epi64($a, $b), _mm256_add_epi64($c, $d)),
433                    $e,
434                )
435            };
436        }
437        let d = [
438            add5!(mul!(va[0], vb[0]), mul!(va[1], vs[4]), mul!(va[2], vs[3]), mul!(va[3], vs[2]), mul!(va[4], vs[1])),
439            add5!(mul!(va[0], vb[1]), mul!(va[1], vb[0]), mul!(va[2], vs[4]), mul!(va[3], vs[3]), mul!(va[4], vs[2])),
440            add5!(mul!(va[0], vb[2]), mul!(va[1], vb[1]), mul!(va[2], vb[0]), mul!(va[3], vs[4]), mul!(va[4], vs[3])),
441            add5!(mul!(va[0], vb[3]), mul!(va[1], vb[2]), mul!(va[2], vb[1]), mul!(va[3], vb[0]), mul!(va[4], vs[4])),
442            add5!(mul!(va[0], vb[4]), mul!(va[1], vb[3]), mul!(va[2], vb[2]), mul!(va[3], vb[1]), mul!(va[4], vb[0])),
443        ];
444        *h = poly1305_reduce([
445            poly1305_hsum(d[0]),
446            poly1305_hsum(d[1]),
447            poly1305_hsum(d[2]),
448            poly1305_hsum(d[3]),
449            poly1305_hsum(d[4]),
450        ]);
451    }
452    groups * 64
453}
454
455/// AVX2 Poly1305: 4 message blocks per group in parallel, then a scalar tail.
456#[cfg(target_arch = "x86_64")]
457#[target_feature(enable = "avx2")]
458unsafe fn poly1305_avx2(key: &[u8; 32], msg: &[u8]) -> [u8; 16] {
459    let r = poly1305_clamp(key);
460    let (vb, vs) = poly1305_vectors(&r);
461    let mut h = [0u32; 5];
462    let mut i = poly1305_groups(&mut h, msg, &vb, &vs);
463    while i < msg.len() {
464        poly1305_absorb(&mut h, msg, i, &r);
465        i += 16;
466    }
467    poly1305_finalize(h, key)
468}
469
470/// Absorb one AEAD block: up to 16 bytes, zero-padded to 16, ALWAYS with the 2¹²⁸ marker — the AEAD
471/// MAC zero-pads `aad`/`ct` to 16-byte boundaries, so every block is "full" (unlike generic
472/// Poly1305, which appends a `1` to a short final block).
473#[inline]
474fn poly1305_absorb_padded(h: &mut [u32; 5], data: &[u8], r: &[u32; 5]) {
475    let n = data.len().min(16);
476    let mut b = [0u8; 16];
477    b[..n].copy_from_slice(&data[..n]);
478    let m = poly1305_block(&b, 1);
479    let sum = [h[0] + m[0], h[1] + m[1], h[2] + m[2], h[3] + m[3], h[4] + m[4]];
480    *h = poly1305_mul(&sum, r);
481}
482
483/// The AEAD MAC over `aad ‖ pad ‖ ct ‖ pad ‖ len(aad) ‖ len(ct)`, STREAMED — no `mac_data` buffer.
484/// `ct` runs through the AVX2 4-block path in place (the dominant cost on bulk); `aad`, the padded
485/// tails, and the length block are scalar. Bit-identical to `poly1305(otk, &mac_data(aad, ct))`.
486fn poly1305_aead(otk: &[u8; 32], aad: &[u8], ct: &[u8]) -> [u8; 16] {
487    let r = poly1305_clamp(otk);
488    let mut h = [0u32; 5];
489    let mut a = 0;
490    while a < aad.len() {
491        poly1305_absorb_padded(&mut h, &aad[a..(a + 16).min(aad.len())], &r);
492        a += 16;
493    }
494    let mut i = 0usize;
495    #[cfg(target_arch = "x86_64")]
496    {
497        if ct.len() >= 256 && std::is_x86_feature_detected!("avx2") {
498            unsafe {
499                let (vb, vs) = poly1305_vectors(&r);
500                i = poly1305_groups(&mut h, ct, &vb, &vs);
501            }
502        }
503    }
504    while i < ct.len() {
505        poly1305_absorb_padded(&mut h, &ct[i..(i + 16).min(ct.len())], &r);
506        i += 16;
507    }
508    let mut lb = [0u8; 16];
509    lb[0..8].copy_from_slice(&(aad.len() as u64).to_le_bytes());
510    lb[8..16].copy_from_slice(&(ct.len() as u64).to_le_bytes());
511    poly1305_absorb_padded(&mut h, &lb, &r);
512    poly1305_finalize(h, otk)
513}
514
515// ── ChaCha20-Poly1305 AEAD (RFC 8439 §2.8) ─────────────────────────────────────────────────────
516
517fn poly1305_key_gen(key: &[u8; 32], nonce: &[u8; 12]) -> [u8; 32] {
518    let block = chacha20_block(&le_u32x8(key), 0, &le_u32x3(nonce));
519    let mut otk = [0u8; 32];
520    for i in 0..8 {
521        otk[i * 4..i * 4 + 4].copy_from_slice(&block[i].to_le_bytes());
522    }
523    otk
524}
525
526/// The MAC input `aad ‖ pad ‖ ct ‖ pad ‖ len(aad) ‖ len(ct)`, materialized. Kept only as the
527/// reference the streaming `poly1305_aead` is fuzzed against — the shipped seal/open stream instead.
528#[cfg(test)]
529fn mac_data(aad: &[u8], ciphertext: &[u8]) -> Vec<u8> {
530    let mut m = Vec::with_capacity(aad.len() + ciphertext.len() + 32);
531    m.extend_from_slice(aad);
532    m.resize(m.len() + ((16 - aad.len() % 16) % 16), 0);
533    m.extend_from_slice(ciphertext);
534    m.resize(m.len() + ((16 - ciphertext.len() % 16) % 16), 0);
535    m.extend_from_slice(&(aad.len() as u64).to_le_bytes());
536    m.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
537    m
538}
539
540/// AEAD seal: returns `ciphertext ‖ tag[16]`.
541pub fn chacha20poly1305_seal(key: &[u8; 32], nonce: &[u8; 12], aad: &[u8], plaintext: &[u8]) -> Vec<u8> {
542    let otk = poly1305_key_gen(key, nonce);
543    let mut out = chacha20_xor(key, nonce, 1, plaintext);
544    let tag = poly1305_aead(&otk, aad, &out);
545    out.extend_from_slice(&tag);
546    out
547}
548
549/// AEAD open: verifies the tag (constant-time) and decrypts, or `None` on tamper/truncation.
550pub fn chacha20poly1305_open(
551    key: &[u8; 32],
552    nonce: &[u8; 12],
553    aad: &[u8],
554    sealed: &[u8],
555) -> Option<Vec<u8>> {
556    if sealed.len() < 16 {
557        return None;
558    }
559    let (ciphertext, tag) = sealed.split_at(sealed.len() - 16);
560    let otk = poly1305_key_gen(key, nonce);
561    let expected = poly1305_aead(&otk, aad, ciphertext);
562    let mut diff = 0u8;
563    for i in 0..16 {
564        diff |= expected[i] ^ tag[i];
565    }
566    if diff != 0 {
567        return None;
568    }
569    Some(chacha20_xor(key, nonce, 1, ciphertext))
570}
571
572// ── Logos-facing wrappers (Seq of Int bytes 0..255) — the natives crypto.lg's aeadSeal/Open call ──
573
574fn bytes(s: &[i64]) -> Vec<u8> {
575    s.iter().map(|&x| x.rem_euclid(256) as u8).collect()
576}
577fn seq(v: &[u8]) -> logicaffeine_data::LogosSeq<i64> {
578    logicaffeine_data::LogosSeq::from_vec(v.iter().map(|&b| b as i64).collect())
579}
580
581/// `chacha20Encrypt(key, nonce, counter, data)` → `data ⊕ keystream`. The cipher; the AEAD flow in
582/// Logos calls it for both the Poly1305 one-time key (counter 0 over 32 zeros) and the payload
583/// (counter 1).
584pub fn chacha20_encrypt_seq(
585    key: &[i64],
586    nonce: &[i64],
587    counter: i64,
588    data: &[i64],
589) -> logicaffeine_data::LogosSeq<i64> {
590    let k: [u8; 32] = bytes(key)[..32].try_into().unwrap();
591    let n: [u8; 12] = bytes(nonce)[..12].try_into().unwrap();
592    seq(&chacha20_xor(&k, &n, counter as u32, &bytes(data)))
593}
594
595/// `poly1305Mac(key, msg)` → 16-byte tag. The MAC primitive (constant-time 5×26-bit limbs).
596pub fn poly1305_seq(key: &[i64], msg: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
597    let k: [u8; 32] = bytes(key)[..32].try_into().unwrap();
598    seq(&poly1305(&k, &bytes(msg)))
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    fn hex(b: &[u8]) -> String {
606        b.iter().map(|x| format!("{x:02x}")).collect()
607    }
608    fn unhex(s: &str) -> Vec<u8> {
609        (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
610    }
611
612    #[test]
613    fn chacha20_avx2_matches_scalar_over_lengths() {
614        // The 8-block AVX2 path must be byte-identical to the scalar block across chunk boundaries,
615        // partial tails, and counter wrap-around.
616        let key: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7));
617        let nonce: [u8; 12] = std::array::from_fn(|i| (i as u8).wrapping_mul(17));
618        let mut s = 0x1234_5678u64;
619        let mut data = vec![0u8; 2000];
620        for b in data.iter_mut() {
621            s = s.wrapping_mul(6364136223846793005).wrapping_add(1);
622            *b = (s >> 33) as u8;
623        }
624        for &len in &[0usize, 1, 63, 64, 65, 511, 512, 513, 1024, 1100, 2000] {
625            for &ctr in &[0u32, 1, 7, u32::MAX - 3] {
626                let want = chacha20_xor_scalar(&key, &nonce, ctr, &data[..len]);
627                let got = chacha20_xor(&key, &nonce, ctr, &data[..len]);
628                assert_eq!(got, want, "AVX2 == scalar, len {len} ctr {ctr}");
629            }
630        }
631    }
632
633    #[test]
634    fn poly1305_avx2_matches_scalar_over_lengths() {
635        // The 4-block AVX2 path must be byte-identical to the scalar block across group boundaries
636        // and tails (the AVX2 path engages at ≥ 256 bytes).
637        let key: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(29).wrapping_add(3));
638        let mut s = 0xdead_beefu64;
639        let mut msg = vec![0u8; 4096];
640        for b in msg.iter_mut() {
641            s = s.wrapping_mul(6364136223846793005).wrapping_add(1);
642            *b = (s >> 33) as u8;
643        }
644        for &len in &[
645            0usize, 1, 16, 17, 63, 64, 255, 256, 257, 271, 512, 1000, 1024, 1025, 4000, 4096,
646        ] {
647            let want = poly1305_scalar(&key, &msg[..len]);
648            let got = poly1305(&key, &msg[..len]);
649            assert_eq!(got, want, "AVX2 == scalar Poly1305, len {len}");
650        }
651    }
652
653    #[test]
654    fn poly1305_aead_streamed_matches_mac_data() {
655        // The streamed AEAD MAC must equal the materialized `poly1305(mac_data(aad, ct))` across all
656        // alignments of aad and ct (full groups, partial tails, AVX2 vs scalar ct).
657        let mut s = 0x0123_4567_89ab_cdefu64;
658        let mut next = |buf: &mut [u8]| {
659            for b in buf.iter_mut() {
660                s = s.wrapping_mul(6364136223846793005).wrapping_add(1);
661                *b = (s >> 33) as u8;
662            }
663        };
664        let mut otk = [0u8; 32];
665        next(&mut otk);
666        let mut ct = vec![0u8; 4100];
667        next(&mut ct);
668        let mut aad = vec![0u8; 64];
669        next(&mut aad);
670        for &al in &[0usize, 1, 11, 16, 17, 32, 48] {
671            for &cl in &[0usize, 1, 15, 16, 63, 64, 255, 256, 257, 271, 1024, 1025, 4100] {
672                let streamed = poly1305_aead(&otk, &aad[..al], &ct[..cl]);
673                let reference = poly1305(&otk, &mac_data(&aad[..al], &ct[..cl]));
674                assert_eq!(streamed, reference, "streamed AEAD MAC, aad {al} ct {cl}");
675            }
676        }
677    }
678
679    #[test]
680    fn chacha20_xor_matches_rfc8439_2_4_2() {
681        // RFC 8439 §2.4.2: the famous "Ladies and Gentlemen..." sunscreen vector.
682        let key: [u8; 32] = std::array::from_fn(|i| i as u8);
683        let nonce: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0x4a, 0, 0, 0, 0];
684        let plaintext = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
685        let ct = chacha20_xor(&key, &nonce, 1, plaintext);
686        assert_eq!(
687            hex(&ct),
688            "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0bf91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d807ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab77937365af90bbf74a35be6b40b8eedf2785e42874d",
689            "ChaCha20 keystream XOR must match RFC 8439 §2.4.2"
690        );
691    }
692
693    #[test]
694    fn poly1305_matches_rfc8439_2_5_2() {
695        let key: [u8; 32] =
696            unhex("85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b")[..32]
697                .try_into()
698                .unwrap();
699        let msg = b"Cryptographic Forum Research Group";
700        let tag = poly1305(&key, msg);
701        assert_eq!(hex(&tag), "a8061dc1305136c6c22b8baf0c0127a9", "Poly1305 must match RFC 8439 §2.5.2");
702    }
703
704    #[test]
705    fn aead_seals_and_opens_round_trip_and_rejects_tamper() {
706        let key: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(3));
707        let nonce: [u8; 12] = std::array::from_fn(|i| i as u8 + 0x40);
708        let aad = b"channel-suite-id-v1";
709        let msg = b"post-quantum sealed wire payload";
710        let sealed = chacha20poly1305_seal(&key, &nonce, aad, msg);
711        assert_eq!(sealed.len(), msg.len() + 16, "ciphertext + 16-byte tag");
712        assert_eq!(chacha20poly1305_open(&key, &nonce, aad, &sealed).as_deref(), Some(&msg[..]));
713        // Tamper the ciphertext, the tag, and the AAD — each must reject.
714        let mut t = sealed.clone();
715        t[0] ^= 1;
716        assert_eq!(chacha20poly1305_open(&key, &nonce, aad, &t), None, "ciphertext tamper rejects");
717        let mut t = sealed.clone();
718        let last = t.len() - 1;
719        t[last] ^= 1;
720        assert_eq!(chacha20poly1305_open(&key, &nonce, aad, &t), None, "tag tamper rejects");
721        assert_eq!(chacha20poly1305_open(&key, &nonce, b"wrong-aad", &sealed), None, "AAD mismatch rejects");
722    }
723
724    #[test]
725    #[ignore = "benchmark — run with --ignored --nocapture"]
726    fn bench_aead_vs_rustcrypto() {
727        use chacha20poly1305::aead::{Aead, KeyInit, Payload};
728        use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
729        use std::time::Instant;
730
731        let key: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1));
732        let nonce: [u8; 12] = std::array::from_fn(|i| i as u8 + 0x10);
733        let aad = b"channel-aad";
734        let cipher = ChaCha20Poly1305::new(Key::from_slice(&key));
735        let n = Nonce::from_slice(&nonce);
736
737        for &len in &[64usize, 1024, 16384] {
738            let pt = vec![0x5au8; len];
739            let sealed = chacha20poly1305_seal(&key, &nonce, aad, &pt);
740            let sealed_o = cipher.encrypt(n, Payload { msg: &pt, aad }).unwrap();
741            assert_eq!(sealed, sealed_o, "our AEAD must match RustCrypto for {len}B");
742
743            const ITERS: u32 = 20000;
744            // Peak throughput = min time over repeats, so a transient scheduling/thermal stall on a
745            // shared box can't masquerade as a slow primitive (both sides measured the same way).
746            macro_rules! mbps {
747                ($body:expr) => {{
748                    for _ in 0..50 { std::hint::black_box($body); }
749                    let mut best = f64::INFINITY;
750                    for _ in 0..15 {
751                        let t = Instant::now();
752                        for _ in 0..ITERS { std::hint::black_box($body); }
753                        best = best.min(t.elapsed().as_secs_f64());
754                    }
755                    (len as f64 * ITERS as f64) / best / 1e6
756                }};
757            }
758            let ours = mbps!(chacha20poly1305_seal(&key, &nonce, aad, &pt));
759            let theirs = mbps!(cipher.encrypt(n, Payload { msg: &pt, aad }).unwrap());
760            eprintln!(
761                "seal {:>6}B:  ours {:>8.0} MB/s   RustCrypto {:>8.0} MB/s   ({:.2}x)",
762                len, ours, theirs, theirs / ours
763            );
764        }
765    }
766
767    #[test]
768    fn aead_matches_rfc8439_2_8_2() {
769        let key: [u8; 32] =
770            unhex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")[..32]
771                .try_into()
772                .unwrap();
773        let nonce: [u8; 12] = unhex("070000004041424344454647")[..12].try_into().unwrap();
774        let aad = unhex("50515253c0c1c2c3c4c5c6c7");
775        let plaintext = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
776        let sealed = chacha20poly1305_seal(&key, &nonce, &aad, plaintext);
777        // RFC 8439 §2.8.2 ciphertext ‖ tag.
778        assert_eq!(
779            hex(&sealed),
780            "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd0600691",
781            "AEAD seal must match RFC 8439 §2.8.2"
782        );
783        assert_eq!(
784            chacha20poly1305_open(&key, &nonce, &aad, &sealed).as_deref(),
785            Some(&plaintext[..]),
786            "AEAD open round-trips the RFC vector"
787        );
788    }
789}