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}