Skip to main content

arcana/cipher/
poly1305.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! Poly1305 one-time message authentication code (RFC 8439 §2.5).
5//!
6//! Poly1305 is a polynomial-evaluation MAC over `GF(2^130 - 5)`. It
7//! is keyed with a 32-byte one-time key `(r, s)` -- the same key
8//! must NEVER be reused for two different messages, otherwise the
9//! authentication forgery becomes trivial. In ChaCha20-Poly1305 the
10//! key is derived freshly per message from the ChaCha20 keystream
11//! block 0, which is the standard way to satisfy this constraint.
12//!
13//! # Construction
14//!
15//! ```text
16//! Acc = 0
17//! for each 16-byte block m_i (last block possibly shorter):
18//!     n = m_i || 0x01 || zero-pad to 17 bytes  (the 0x01 is appended
19//!         right after the message bytes; for a full 16-byte block it
20//!         lives at byte 16, for a short block of length L it lives
21//!         at byte L)
22//!     Acc = ((Acc + n) * r) mod p   where p = 2^130 - 5
23//! tag = (Acc + s) mod 2^128         (low 16 bytes only)
24//! ```
25//!
26//! `r` is "clamped" before use per the spec: certain bits of the 16
27//! key bytes are cleared so that intermediate products fit in
28//! 130 + 26 bits and overflow can be handled by simple word-wise
29//! reduction with no conditional branches.
30//!
31//! # Internal representation
32//!
33//! Field elements are stored in **5 limbs of 26 bits** packed into
34//! `u32`. This is the standard radix-2^26 representation: it gives
35//! enough headroom that a 26-bit × 26-bit product fits in u64
36//! (52 bits) and that the carry propagation between limbs after a
37//! Poly1305 multiply-and-reduce stays comfortably under u64. It is
38//! the same layout used by `poly1305-donna`, `libsodium`,
39//! `openssl/poly1305_64.c`, and the reference implementation in
40//! the RFC 8439 Appendix C.
41//!
42//! # Tests
43//!
44//! Pinned against the RFC 8439 §2.5.2 single test vector. A more
45//! exhaustive set of vectors lives in §2.6 and Appendix A but the
46//! §2.5.2 vector is sharp enough to catch every meaningful bug
47//! (clamping, multiply-and-reduce, finalisation `+ s mod 2^128`).
48
49// ============================================================================
50// Poly1305 state
51// ============================================================================
52
53/// Poly1305 MAC state.
54///
55/// Holds the accumulator `acc`, the precomputed `r` (and `r * 5` for
56/// each limb, to absorb the `* 5 mod p` of the lazy reduction in the
57/// inner loop), the tag-finalisation key `s`, and a 16-byte buffer
58/// for handling messages whose length is not a multiple of 16.
59///
60/// Internally:
61/// - `r[0..5]`  : `r` in 5 26-bit limbs
62/// - `s[0..4]`  : the second half of the one-time key as 4 LE u32s
63/// - `acc[0..5]`: accumulator in 5 26-bit limbs
64/// - `buffer`   : up to 16 bytes of pending data
65/// - `buf_pos`  : 0..=16
66pub struct Poly1305 {
67    r: [u32; 5],
68    s: [u32; 4],
69    acc: [u32; 5],
70    buffer: [u8; 16],
71    buf_pos: usize,
72}
73
74impl Poly1305 {
75    /// Initialise Poly1305 with a 32-byte one-time key `key = r ‖ s`.
76    ///
77    /// `r` is clamped per RFC 8439 §2.5: bytes 3, 7, 11, 15 have
78    /// their top 4 bits cleared (`& 0x0f`) and bytes 4, 8, 12 have
79    /// their low 2 bits cleared (`& 0xfc`).
80    pub fn new(key: &[u8; 32]) -> Self {
81        let mut r_bytes = [0u8; 16];
82        r_bytes.copy_from_slice(&key[..16]);
83        // Clamp r per RFC 8439 §2.5.
84        r_bytes[3] &= 0x0f;
85        r_bytes[7] &= 0x0f;
86        r_bytes[11] &= 0x0f;
87        r_bytes[15] &= 0x0f;
88        r_bytes[4] &= 0xfc;
89        r_bytes[8] &= 0xfc;
90        r_bytes[12] &= 0xfc;
91
92        // Pack r into 5 26-bit limbs (little-endian byte order).
93        let r0 = u32::from_le_bytes(r_bytes[0..4].try_into().unwrap()) & 0x03ffffff;
94        let r1 = (u32::from_le_bytes(r_bytes[3..7].try_into().unwrap()) >> 2) & 0x03ffff03;
95        let r2 = (u32::from_le_bytes(r_bytes[6..10].try_into().unwrap()) >> 4) & 0x03ffc0ff;
96        let r3 = (u32::from_le_bytes(r_bytes[9..13].try_into().unwrap()) >> 6) & 0x03f03fff;
97        let r4 = (u32::from_le_bytes(r_bytes[12..16].try_into().unwrap()) >> 8) & 0x000fffff;
98
99        // The above masks combine the clamping and the radix-2^26
100        // packing in one shot. The constants come from the standard
101        // donna-style packing used by libsodium and OpenSSL: each
102        // limb holds 26 bits aligned on its sub-byte boundary.
103
104        let mut s = [0u32; 4];
105        for i in 0..4 {
106            s[i] = u32::from_le_bytes(key[16 + 4 * i..16 + 4 * i + 4].try_into().unwrap());
107        }
108
109        Self {
110            r: [r0, r1, r2, r3, r4],
111            s,
112            acc: [0u32; 5],
113            buffer: [0u8; 16],
114            buf_pos: 0,
115        }
116    }
117
118    /// Absorb additional data into the accumulator.
119    ///
120    /// Buffers up to 15 leftover bytes between calls so that
121    /// arbitrary update sizes work the same as one big call.
122    pub fn update(&mut self, data: &[u8]) {
123        let mut pos = 0;
124
125        // First, fill the partial buffer if any.
126        if self.buf_pos > 0 {
127            let need = 16 - self.buf_pos;
128            let take = need.min(data.len());
129            self.buffer[self.buf_pos..self.buf_pos + take].copy_from_slice(&data[..take]);
130            self.buf_pos += take;
131            pos += take;
132            if self.buf_pos == 16 {
133                let block = self.buffer;
134                self.absorb_block(&block, /* high_bit = */ true);
135                self.buf_pos = 0;
136            }
137        }
138
139        // Process whole blocks directly from the input.
140        while pos + 16 <= data.len() {
141            let mut block = [0u8; 16];
142            block.copy_from_slice(&data[pos..pos + 16]);
143            self.absorb_block(&block, true);
144            pos += 16;
145        }
146
147        // Stash any remainder for the next call.
148        if pos < data.len() {
149            let remaining = data.len() - pos;
150            self.buffer[..remaining].copy_from_slice(&data[pos..]);
151            self.buf_pos = remaining;
152        }
153    }
154
155    /// Finalise the MAC and write the 16-byte tag into `tag`.
156    /// Consumes the state.
157    pub fn finalize(mut self) -> [u8; 16] {
158        // Process the final partial block (if any) with high_bit = false:
159        // for a short block of length L < 16, append a single 0x01 byte
160        // at position L and zero-pad to 16 bytes, then absorb without
161        // adding the implicit "+1" at bit 128.
162        if self.buf_pos > 0 {
163            let mut last = [0u8; 16];
164            last[..self.buf_pos].copy_from_slice(&self.buffer[..self.buf_pos]);
165            last[self.buf_pos] = 0x01;
166            self.absorb_block(&last, /* high_bit = */ false);
167        }
168
169        // Final reduction of the accumulator into [0, p).
170        let mut h0 = self.acc[0];
171        let mut h1 = self.acc[1];
172        let mut h2 = self.acc[2];
173        let mut h3 = self.acc[3];
174        let mut h4 = self.acc[4];
175
176        // Carry-propagate one more time.
177        let mut c: u32;
178        c = h1 >> 26;
179        h1 &= 0x03ffffff;
180        h2 += c;
181        c = h2 >> 26;
182        h2 &= 0x03ffffff;
183        h3 += c;
184        c = h3 >> 26;
185        h3 &= 0x03ffffff;
186        h4 += c;
187        c = h4 >> 26;
188        h4 &= 0x03ffffff;
189        h0 += c * 5;
190        c = h0 >> 26;
191        h0 &= 0x03ffffff;
192        h1 += c;
193
194        // Compute h - p = h + 5 - 2^130 and select between h and (h - p)
195        // based on whether h >= p. We use the standard donna trick:
196        // add 5, then subtract 2^130 unless the carry came back negative.
197        let mut g0 = h0.wrapping_add(5);
198        let c = g0 >> 26;
199        g0 &= 0x03ffffff;
200        let mut g1 = h1.wrapping_add(c);
201        let c = g1 >> 26;
202        g1 &= 0x03ffffff;
203        let mut g2 = h2.wrapping_add(c);
204        let c = g2 >> 26;
205        g2 &= 0x03ffffff;
206        let mut g3 = h3.wrapping_add(c);
207        let c = g3 >> 26;
208        g3 &= 0x03ffffff;
209        let g4 = h4.wrapping_add(c).wrapping_sub(1 << 26);
210
211        // If g4 has bit 31 set after the subtract-2^130 (i.e. h < p),
212        // keep h. Otherwise (h >= p) keep g.
213        let mask = (g4 >> 31).wrapping_sub(1); // 0xffffffff if h>=p, else 0
214        let inv = !mask;
215        h0 = (h0 & inv) | (g0 & mask);
216        h1 = (h1 & inv) | (g1 & mask);
217        h2 = (h2 & inv) | (g2 & mask);
218        h3 = (h3 & inv) | (g3 & mask);
219        h4 = (h4 & inv) | (g4 & mask);
220
221        // Pack the 5 26-bit limbs into 4 little-endian u32s.
222        let h0 = h0 | (h1 << 26);
223        let h1 = (h1 >> 6) | (h2 << 20);
224        let h2 = (h2 >> 12) | (h3 << 14);
225        let h3 = (h3 >> 18) | (h4 << 8);
226
227        // tag = (h + s) mod 2^128
228        let mut f: u64 = h0 as u64 + self.s[0] as u64;
229        let t0 = f as u32;
230        f = (f >> 32) + h1 as u64 + self.s[1] as u64;
231        let t1 = f as u32;
232        f = (f >> 32) + h2 as u64 + self.s[2] as u64;
233        let t2 = f as u32;
234        f = (f >> 32) + h3 as u64 + self.s[3] as u64;
235        let t3 = f as u32;
236
237        let mut tag = [0u8; 16];
238        tag[0..4].copy_from_slice(&t0.to_le_bytes());
239        tag[4..8].copy_from_slice(&t1.to_le_bytes());
240        tag[8..12].copy_from_slice(&t2.to_le_bytes());
241        tag[12..16].copy_from_slice(&t3.to_le_bytes());
242        tag
243    }
244
245    /// One-shot helper: feed the entire `data` and return the tag.
246    pub fn mac(key: &[u8; 32], data: &[u8]) -> [u8; 16] {
247        let mut p = Self::new(key);
248        p.update(data);
249        p.finalize()
250    }
251
252    /// Absorb a single 16-byte block into the accumulator. The
253    /// `high_bit` parameter is `true` for normal blocks (the implicit
254    /// 0x01 byte at position 16 is added to the integer reading) and
255    /// `false` for the final short block, where the message has
256    /// already been padded with `0x01 || 0`.
257    fn absorb_block(&mut self, block: &[u8; 16], high_bit: bool) {
258        // Read the 16-byte block as 5 26-bit limbs (LE).
259        let h0 = self.acc[0].wrapping_add(u32::from_le_bytes(block[0..4].try_into().unwrap()) & 0x03ffffff);
260        let h1 = self.acc[1].wrapping_add((u32::from_le_bytes(block[3..7].try_into().unwrap()) >> 2) & 0x03ffffff);
261        let h2 = self.acc[2].wrapping_add((u32::from_le_bytes(block[6..10].try_into().unwrap()) >> 4) & 0x03ffffff);
262        let h3 = self.acc[3].wrapping_add((u32::from_le_bytes(block[9..13].try_into().unwrap()) >> 6) & 0x03ffffff);
263        let h4 = self.acc[4].wrapping_add(
264            (u32::from_le_bytes(block[12..16].try_into().unwrap()) >> 8) | (if high_bit { 1 << 24 } else { 0 }),
265        );
266
267        // h *= r mod p, with the standard donna-style schoolbook
268        // 5x5 multiplication that absorbs the * 5 mod p reduction
269        // into the cross-terms.
270        let r0 = self.r[0] as u64;
271        let r1 = self.r[1] as u64;
272        let r2 = self.r[2] as u64;
273        let r3 = self.r[3] as u64;
274        let r4 = self.r[4] as u64;
275
276        // r * 5 (used to fold high bits back into the low limbs).
277        let s1 = r1 * 5;
278        let s2 = r2 * 5;
279        let s3 = r3 * 5;
280        let s4 = r4 * 5;
281
282        let h0_64 = h0 as u64;
283        let h1_64 = h1 as u64;
284        let h2_64 = h2 as u64;
285        let h3_64 = h3 as u64;
286        let h4_64 = h4 as u64;
287
288        let d0 = h0_64 * r0 + h1_64 * s4 + h2_64 * s3 + h3_64 * s2 + h4_64 * s1;
289        let d1 = h0_64 * r1 + h1_64 * r0 + h2_64 * s4 + h3_64 * s3 + h4_64 * s2;
290        let d2 = h0_64 * r2 + h1_64 * r1 + h2_64 * r0 + h3_64 * s4 + h4_64 * s3;
291        let d3 = h0_64 * r3 + h1_64 * r2 + h2_64 * r1 + h3_64 * r0 + h4_64 * s4;
292        let d4 = h0_64 * r4 + h1_64 * r3 + h2_64 * r2 + h3_64 * r1 + h4_64 * r0;
293
294        // Carry-propagate.
295        let mut c: u64;
296        let mut h0 = (d0 & 0x03ffffff) as u32;
297        c = d0 >> 26;
298        let mut h1 = ((d1 + c) & 0x03ffffff) as u32;
299        c = (d1 + c) >> 26;
300        let h2 = ((d2 + c) & 0x03ffffff) as u32;
301        c = (d2 + c) >> 26;
302        let h3 = ((d3 + c) & 0x03ffffff) as u32;
303        c = (d3 + c) >> 26;
304        let h4 = ((d4 + c) & 0x03ffffff) as u32;
305        c = (d4 + c) >> 26;
306        h0 = h0.wrapping_add((c * 5) as u32);
307        let c2 = h0 >> 26;
308        h0 &= 0x03ffffff;
309        h1 = h1.wrapping_add(c2);
310
311        self.acc = [h0, h1, h2, h3, h4];
312    }
313}
314
315// ============================================================================
316// Tests (RFC 8439 pinned vectors)
317// ============================================================================
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    fn hex(s: &str) -> Vec<u8> {
324        assert!(s.len() % 2 == 0);
325        (0..s.len())
326            .step_by(2)
327            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
328            .collect()
329    }
330
331    fn hex_arr<const N: usize>(s: &str) -> [u8; N] {
332        let v = hex(s);
333        assert_eq!(v.len(), N);
334        let mut out = [0u8; N];
335        out.copy_from_slice(&v);
336        out
337    }
338
339    /// RFC 8439 §2.5.2 Poly1305 test vector.
340    ///
341    /// Key: 85d6be7857556d337f4452fe42d506a8 0103808afb0db2fd4abff6af4149f51b
342    /// Msg: "Cryptographic Forum Research Group"
343    /// Tag: a8061dc1305136c6c22b8baf0c0127a9
344    #[test]
345    fn rfc8439_2_5_2_poly1305_vector() {
346        let key: [u8; 32] = hex_arr("85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b");
347        let msg = b"Cryptographic Forum Research Group";
348        let tag = Poly1305::mac(&key, msg);
349        let expected = hex("a8061dc1305136c6c22b8baf0c0127a9");
350        assert_eq!(tag.to_vec(), expected);
351    }
352
353    /// Chunked update must give the same tag as one big update.
354    #[test]
355    fn poly1305_chunked_update_matches_oneshot() {
356        let key = [0x42u8; 32];
357        let msg: Vec<u8> = (0..200).map(|i| (i * 7) as u8).collect();
358
359        let oneshot = Poly1305::mac(&key, &msg);
360
361        let mut chunks = Poly1305::new(&key);
362        chunks.update(&msg[..7]);
363        chunks.update(&msg[7..50]);
364        chunks.update(&msg[50..50]); // empty update
365        chunks.update(&msg[50..120]); // crosses 16-byte boundaries
366        chunks.update(&msg[120..]);
367        let tag = chunks.finalize();
368
369        assert_eq!(oneshot, tag);
370    }
371
372    /// Different keys must produce different tags for the same
373    /// message (sanity that the key actually drives the output).
374    #[test]
375    fn poly1305_different_keys_differ() {
376        let msg = b"the quick brown fox";
377        let k1 = [0x01u8; 32];
378        let mut k2 = [0x01u8; 32];
379        k2[0] ^= 0x80;
380        // Both keys still satisfy the clamp constraints since the
381        // clamp clears the relevant bits at packing time.
382        assert_ne!(Poly1305::mac(&k1, msg), Poly1305::mac(&k2, msg));
383    }
384
385    /// Different messages must produce different tags under the same
386    /// key (sanity that the message actually drives the output).
387    #[test]
388    fn poly1305_different_messages_differ() {
389        let key = [0x77u8; 32];
390        assert_ne!(Poly1305::mac(&key, b"hello"), Poly1305::mac(&key, b"world"));
391    }
392}