Skip to main content

cryptography/modes/
eax.rs

1//! EAX authenticated encryption mode.
2//!
3//! EAX combines CMAC and CTR for nonce-based authenticated encryption over
4//! 128-bit block ciphers.
5
6use crate::BlockCipher;
7
8#[inline]
9fn increment_be(counter: &mut [u8; 16]) {
10    for b in counter.iter_mut().rev() {
11        let (next, carry) = b.overflowing_add(1);
12        *b = next;
13        if !carry {
14            break;
15        }
16    }
17}
18
19#[inline]
20fn xor_in_place(dst: &mut [u8], src: &[u8]) {
21    for (d, s) in dst.iter_mut().zip(src.iter()) {
22        *d ^= *s;
23    }
24}
25
26#[inline]
27fn rb_for(block_len: usize) -> u8 {
28    match block_len {
29        8 => 0x1b,
30        16 => 0x87,
31        _ => panic!("CMAC only supports 64-bit or 128-bit block ciphers"),
32    }
33}
34
35fn dbl(block: &[u8]) -> Vec<u8> {
36    let mut out = vec![0u8; block.len()];
37    let mut carry = 0u8;
38    for (o, &b) in out.iter_mut().rev().zip(block.iter().rev()) {
39        *o = (b << 1) | carry;
40        carry = b >> 7;
41    }
42    if carry != 0 {
43        let last = out.len() - 1;
44        out[last] ^= rb_for(block.len());
45    }
46    out
47}
48
49fn cmac_compute<C: BlockCipher>(cipher: &C, data: &[u8]) -> [u8; 16] {
50    assert_eq!(C::BLOCK_LEN, 16, "EAX requires a 128-bit block cipher");
51    let blk = C::BLOCK_LEN;
52    let mut l = vec![0u8; blk];
53    cipher.encrypt(&mut l);
54    let k1 = dbl(&l);
55    let k2 = dbl(&k1);
56
57    let n = if data.is_empty() {
58        1
59    } else {
60        data.len().div_ceil(blk)
61    };
62    let last_complete = !data.is_empty() && data.len().is_multiple_of(blk);
63
64    let mut x = vec![0u8; blk];
65    let mut y = vec![0u8; blk];
66
67    for block in data.chunks(blk).take(n.saturating_sub(1)) {
68        y.copy_from_slice(&x);
69        xor_in_place(&mut y, block);
70        cipher.encrypt(&mut y);
71        x.copy_from_slice(&y);
72    }
73
74    let mut m_last = vec![0u8; blk];
75    if last_complete {
76        let start = (n - 1) * blk;
77        m_last.copy_from_slice(&data[start..start + blk]);
78        xor_in_place(&mut m_last, &k1);
79    } else {
80        let start = (n - 1) * blk;
81        let rem = data.len().saturating_sub(start);
82        if rem != 0 {
83            m_last[..rem].copy_from_slice(&data[start..]);
84        }
85        m_last[rem] = 0x80;
86        xor_in_place(&mut m_last, &k2);
87    }
88
89    xor_in_place(&mut m_last, &x);
90    cipher.encrypt(&mut m_last);
91    m_last.try_into().expect("CMAC output is one block")
92}
93
94fn eax_omac<C: BlockCipher>(cipher: &C, domain: u8, data: &[u8]) -> [u8; 16] {
95    let mut prefixed = Vec::with_capacity(16 + data.len());
96    prefixed.extend_from_slice(&[0u8; 15]);
97    prefixed.push(domain);
98    prefixed.extend_from_slice(data);
99    cmac_compute(cipher, &prefixed)
100}
101
102fn ctr_apply<C: BlockCipher>(cipher: &C, initial_counter: &[u8; 16], data: &mut [u8]) {
103    let mut counter = *initial_counter;
104    for chunk in data.chunks_mut(16) {
105        let mut stream = counter;
106        cipher.encrypt(&mut stream);
107        for i in 0..chunk.len() {
108            chunk[i] ^= stream[i];
109        }
110        increment_be(&mut counter);
111    }
112}
113
114/// EAX AEAD with a full 16-byte detached tag.
115pub struct Eax<C> {
116    cipher: C,
117}
118
119impl<C> Eax<C> {
120    /// Wrap a 128-bit block cipher in EAX mode.
121    pub fn new(cipher: C) -> Self {
122        Self { cipher }
123    }
124
125    /// Borrow the wrapped cipher.
126    pub fn cipher(&self) -> &C {
127        &self.cipher
128    }
129}
130
131impl<C: BlockCipher> Eax<C> {
132    /// Encrypt `data` in place and return a detached 16-byte tag.
133    #[must_use]
134    pub fn encrypt(&self, nonce: &[u8], aad: &[u8], data: &mut [u8]) -> [u8; 16] {
135        assert_eq!(C::BLOCK_LEN, 16, "EAX requires a 128-bit block cipher");
136
137        let n_tag = eax_omac(&self.cipher, 0, nonce);
138        let h_tag = eax_omac(&self.cipher, 1, aad);
139
140        ctr_apply(&self.cipher, &n_tag, data);
141        let c_tag = eax_omac(&self.cipher, 2, data);
142
143        let mut tag = [0u8; 16];
144        for i in 0..16 {
145            tag[i] = n_tag[i] ^ h_tag[i] ^ c_tag[i];
146        }
147        tag
148    }
149
150    /// Verify and decrypt `data` in place.
151    ///
152    /// Returns `false` and leaves `data` unchanged on authentication failure.
153    pub fn decrypt(&self, nonce: &[u8], aad: &[u8], data: &mut [u8], tag: &[u8; 16]) -> bool {
154        assert_eq!(C::BLOCK_LEN, 16, "EAX requires a 128-bit block cipher");
155
156        let n_tag = eax_omac(&self.cipher, 0, nonce);
157        let h_tag = eax_omac(&self.cipher, 1, aad);
158        let c_tag = eax_omac(&self.cipher, 2, data);
159        let mut expected = [0u8; 16];
160        for i in 0..16 {
161            expected[i] = n_tag[i] ^ h_tag[i] ^ c_tag[i];
162        }
163
164        if crate::ct::constant_time_eq_mask(&expected, tag) != u8::MAX {
165            return false;
166        }
167
168        ctr_apply(&self.cipher, &n_tag, data);
169        true
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::Eax;
176    use crate::Aes128;
177
178    fn unhex_ws(input: &str) -> Vec<u8> {
179        let compact: String = input.chars().filter(|c| !c.is_whitespace()).collect();
180        let mut out = Vec::with_capacity(compact.len() / 2);
181        let bytes = compact.as_bytes();
182        let mut i = 0usize;
183        while i + 1 < bytes.len() {
184            let hi = (bytes[i] as char).to_digit(16).expect("hex") as u8;
185            let lo = (bytes[i + 1] as char).to_digit(16).expect("hex") as u8;
186            out.push((hi << 4) | lo);
187            i += 2;
188        }
189        out
190    }
191
192    #[test]
193    fn eax_aes128_eprint_2003_069_known_vectors() {
194        // Official EAX KATs from Bellare-Rogaway-Wagner, "A Conventional
195        // Authenticated-Encryption Mode" (ePrint 2003/069), mirrored in
196        // Wycheproof as `Ktv` cases for AES-EAX.
197        //
198        // Format per vector: key, nonce, aad, plaintext, ciphertext, tag.
199        let vectors = [
200            (
201                "233952dee4d5ed5f9b9c6d6ff80ff478",
202                "62ec67f9c3a4a407fcb2a8c49031a8b3",
203                "6bfb914fd07eae6b",
204                "",
205                "",
206                "e037830e8389f27b025a2d6527e79d01",
207            ),
208            (
209                "91945d3f4dcbee0bf45ef52255f095a4",
210                "becaf043b0a23d843194ba972c66debd",
211                "fa3bfd4806eb53fa",
212                "f7fb",
213                "19dd",
214                "5c4c9331049d0bdab0277408f67967e5",
215            ),
216            (
217                "01f74ad64077f2e704c0f60ada3dd523",
218                "70c3db4f0d26368400a10ed05d2bff5e",
219                "234a3463c1264ac6",
220                "1a47cb4933",
221                "d851d5bae0",
222                "3a59f238a23e39199dc9266626c40f80",
223            ),
224            (
225                "d07cf6cbb7f313bdde66b727afd3c5e8",
226                "8408dfff3c1a2b1292dc199e46b7d617",
227                "33cce2eabff5a79d",
228                "481c9e39b1",
229                "632a9d131a",
230                "d4c168a4225d8e1ff755939974a7bede",
231            ),
232            (
233                "35b6d0580005bbc12b0587124557d2c2",
234                "fdb6b06676eedc5c61d74276e1f8e816",
235                "aeb96eaebe2970e9",
236                "40d0c07da5e4",
237                "071dfe16c675",
238                "cb0677e536f73afe6a14b74ee49844dd",
239            ),
240            (
241                "bd8e6e11475e60b268784c38c62feb22",
242                "6eac5c93072d8e8513f750935e46da1b",
243                "d4482d1ca78dce0f",
244                "4de3b35c3fc039245bd1fb7d",
245                "835bb4f15d743e350e728414",
246                "abb8644fd6ccb86947c5e10590210a4f",
247            ),
248            (
249                "7c77d6e813bed5ac98baa417477a2e7d",
250                "1a8c98dcd73d38393b2bf1569deefc19",
251                "65d2017990d62528",
252                "8b0a79306c9ce7ed99dae4f87f8dd61636",
253                "02083e3979da014812f59f11d52630da30",
254                "137327d10649b0aa6e1c181db617d7f2",
255            ),
256            (
257                "5fff20cafab119ca2fc73549e20f5b0d",
258                "dde59b97d722156d4d9aff2bc7559826",
259                "54b9f04e6a09189a",
260                "1bda122bce8a8dbaf1877d962b8592dd2d56",
261                "2ec47b2c4954a489afc7ba4897edcdae8cc3",
262                "3b60450599bd02c96382902aef7f832a",
263            ),
264            (
265                "a4a4782bcffd3ec5e7ef6d8c34a56123",
266                "b781fcf2f75fa5a8de97a9ca48e522ec",
267                "899a175897561d7e",
268                "6cf36720872b8513f6eab1a8a44438d5ef11",
269                "0de18fd0fdd91e7af19f1d8ee8733938b1e8",
270                "e7f6d2231618102fdb7fe55ff1991700",
271            ),
272            (
273                "8395fcf1e95bebd697bd010bc766aac3",
274                "22e7add93cfc6393c57ec0b3c17d6b44",
275                "126735fcc320d25a",
276                "ca40d7446e545ffaed3bd12a740a659ffbbb3ceab7",
277                "cb8920f87a6c75cff39627b56e3ed197c552d295a7",
278                "cfc46afc253b4652b1af3795b124ab6e",
279            ),
280        ];
281
282        for (idx, (key_hex, nonce_hex, aad_hex, pt_hex, ct_hex, tag_hex)) in
283            vectors.iter().enumerate()
284        {
285            let key = <[u8; 16]>::try_from(unhex_ws(key_hex)).expect("16-byte key");
286            let nonce = unhex_ws(nonce_hex);
287            let aad = unhex_ws(aad_hex);
288            let mut plaintext = unhex_ws(pt_hex);
289            let expected_ciphertext = unhex_ws(ct_hex);
290            let expected_tag = <[u8; 16]>::try_from(unhex_ws(tag_hex)).expect("16-byte tag");
291
292            let eax = Eax::new(Aes128::new(&key));
293            let tag = eax.encrypt(&nonce, &aad, &mut plaintext);
294            assert_eq!(
295                plaintext, expected_ciphertext,
296                "ciphertext mismatch for EAX KAT #{idx}"
297            );
298            assert_eq!(tag, expected_tag, "tag mismatch for EAX KAT #{idx}");
299
300            assert!(
301                eax.decrypt(&nonce, &aad, &mut plaintext, &tag),
302                "decrypt failed for EAX KAT #{idx}"
303            );
304            assert_eq!(
305                plaintext,
306                unhex_ws(pt_hex),
307                "plaintext mismatch after decrypt for EAX KAT #{idx}"
308            );
309        }
310    }
311
312    #[test]
313    fn eax_tamper_rejected() {
314        let key = [0x11u8; 16];
315        let nonce = [0x22u8; 16];
316        let aad = b"aad";
317        let mut data = b"eax data".to_vec();
318        let eax = Eax::new(Aes128::new(&key));
319        let tag = eax.encrypt(&nonce, aad, &mut data);
320
321        data[0] ^= 1;
322        let snapshot = data.clone();
323        assert!(!eax.decrypt(&nonce, aad, &mut data, &tag));
324        assert_eq!(data, snapshot);
325    }
326
327    #[test]
328    fn eax_roundtrip_various_lengths() {
329        let key = [0x42u8; 16];
330        let eax = Eax::new(Aes128::new(&key));
331        for msg_len in [0usize, 1, 2, 15, 16, 17, 31, 32, 33] {
332            let nonce = vec![0x24; 13];
333            let aad = vec![0x35; 11];
334            let mut data = vec![0u8; msg_len];
335            for (i, b) in data.iter_mut().enumerate() {
336                *b = u8::try_from(i & 0xff).expect("byte");
337            }
338            let original = data.clone();
339            let tag = eax.encrypt(&nonce, &aad, &mut data);
340            assert!(eax.decrypt(&nonce, &aad, &mut data, &tag));
341            assert_eq!(data, original);
342        }
343    }
344}