Skip to main content

arcana/cipher/
ocb.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! AES-OCB authenticated encryption (OCB3, RFC 7253).
5//!
6//! OCB is a single-pass, parallelizable AEAD built on a 128-bit block
7//! cipher (here AES-128/192/256, via the existing [`Aes`] core). This
8//! module implements **OCB3 exactly as specified in RFC 7253**: the
9//! `L_*`, `L_$`, `L_i` doubling in GF(2¹²⁸), the incremental `Offset`
10//! update driven by `ntz(i)`, the `Checksum`, the PMAC-style
11//! associated-data hash `HASH`, and the `Stretch`/`Nonce` bottom
12//! rotation for the initial offset.
13//!
14//! Encryption returns `ciphertext ‖ tag`; decryption verifies the tag
15//! in constant time and returns the plaintext only on success — a
16//! wrong tag never releases plaintext.
17//!
18//! Only the standard 16-byte (128-bit) tag and the RFC's default
19//! block cipher (AES) are exposed here. Nonces of 1..=15 bytes are
20//! accepted per RFC 7253 §4.2 (`TAGLEN` is fixed at 128, so the low
21//! byte of the nonce-format block is `0`).
22//!
23//! # Construction (RFC 7253 §4-5)
24//!
25//! Setup (once per key, [`Ocb::new`]):
26//! - `L_* = ENCIPHER(K, zeros(128))`
27//! - `L_$ = double(L_*)`
28//! - `L_0 = double(L_$)`, `L_i = double(L_{i-1})`
29//!
30//! Per message the initial offset comes from the nonce:
31//! - `Nonce = num2str(TAGLEN mod 128, 7) ‖ zeros(120 − 8·|N|) ‖ 1 ‖ N`
32//! - `bottom = str2num(Nonce[123..128])` (low 6 bits)
33//! - `Ktop = ENCIPHER(K, Nonce with last 6 bits zeroed)`
34//! - `Stretch = Ktop ‖ (Ktop[0..8] xor Ktop[1..9])`
35//! - `Offset_0 = Stretch[1+bottom .. 1+bottom+128]` (bit offset)
36//!
37//! # Side-channel posture
38//!
39//! - **Block cipher.** OCB calls the existing table-based AES
40//!   ([`Aes`]); AES's cache-timing gap is the tracked item `T1-A`,
41//!   handled separately. This module does not add or fix any AES
42//!   leakage — it only calls `encrypt_block` / `decrypt_block`.
43//! - **Tag verification.** Decryption compares the recomputed tag
44//!   against the received tag with [`silentops::ct_eq`] — a single
45//!   data-oblivious pass, no early exit, and **no plaintext is
46//!   returned on mismatch** (the decrypted buffer is dropped). A wrong
47//!   tag leaks nothing through timing.
48//! - **Offset / doubling / checksum / HASH.** All are `xor`,
49//!   fixed-shift `double`, and fixed byte moves on public-position
50//!   data; `ntz(i)` and all loop bounds depend only on the (public)
51//!   message and AAD lengths, never on secrets. Constant-time by
52//!   construction.
53//! - **`bottom` rotation.** The `Stretch` bit-rotation is indexed by
54//!   `bottom`, which is derived from the (public) nonce, not from a
55//!   secret. No secret-dependent memory access.
56//!
57//! # References
58//!
59//! - T. Krovetz, P. Rogaway, RFC 7253, "The OCB Authenticated-
60//!   Encryption Algorithm" (2014). <https://www.rfc-editor.org/rfc/rfc7253>
61//! - Test vectors: RFC 7253 Appendix A.
62
63use super::aes::Aes;
64use crate::BlockCipher;
65
66const BLOCK: usize = 16;
67const TAG_LEN: usize = 16;
68
69// ============================================================================
70// GF(2^128) doubling and block XOR helpers (RFC 7253 §2)
71// ============================================================================
72
73/// `double(S)` over GF(2¹²⁸) with the OCB reduction polynomial
74/// `x^128 + x^7 + x^2 + x + 1` (i.e. constant `0x87`), big-endian
75/// bit ordering per RFC 7253 §2.
76///
77/// Constant-time: the top-bit test is folded into a mask (no branch
78/// on secret data — and `S` here is never secret-position anyway).
79#[inline]
80fn double(s: &[u8; BLOCK]) -> [u8; BLOCK] {
81    let msb = s[0] >> 7; // 0 or 1
82    let mut out = [0u8; BLOCK];
83    let mut carry = 0u8;
84    for i in (0..BLOCK).rev() {
85        let cur = s[i];
86        out[i] = (cur << 1) | carry;
87        carry = cur >> 7;
88    }
89    // If the original MSB was set, xor 0x87 into the last byte.
90    out[BLOCK - 1] ^= msb.wrapping_mul(0x87);
91    out
92}
93
94/// In-place `dst ^= src` on a 16-byte block.
95#[inline]
96fn xor_block(dst: &mut [u8; BLOCK], src: &[u8; BLOCK]) {
97    for i in 0..BLOCK {
98        dst[i] ^= src[i];
99    }
100}
101
102/// `a xor b` on 16-byte blocks (returns a fresh block).
103#[inline]
104fn xor_blocks(a: &[u8; BLOCK], b: &[u8; BLOCK]) -> [u8; BLOCK] {
105    let mut out = *a;
106    xor_block(&mut out, b);
107    out
108}
109
110/// Number of trailing zero bits of a 1-based block index `i`
111/// (`ntz(i)`, RFC 7253 §2). `i >= 1` always here.
112#[inline]
113fn ntz(i: usize) -> u32 {
114    i.trailing_zeros()
115}
116
117// ============================================================================
118// OCB context
119// ============================================================================
120
121/// AES-OCB (OCB3, RFC 7253) with a 128-bit tag.
122///
123/// Holds the AES key schedule and the precomputed `L_*`, `L_$`, and a
124/// small cache of `L_i` doubling values. The cache grows on demand up
125/// to the number of blocks seen; for typical messages a handful of
126/// entries suffice.
127pub struct Ocb {
128    aes: Aes,
129    l_star: [u8; BLOCK],
130    l_dollar: [u8; BLOCK],
131    /// `l[i] = double^{i+1}(L_$)`, i.e. `l[0] = L_0`, `l[1] = L_1`, ...
132    l: Vec<[u8; BLOCK]>,
133}
134
135impl Ocb {
136    /// Create an AES-OCB context. `key` must be 16, 24, or 32 bytes
137    /// (AES-128/192/256). Returns `None` for an invalid key length.
138    pub fn new(key: &[u8]) -> Option<Self> {
139        if !matches!(key.len(), 16 | 24 | 32) {
140            return None;
141        }
142        let aes = <Aes as BlockCipher>::new(key);
143
144        // L_* = ENCIPHER(K, 0^128)
145        let mut l_star = [0u8; BLOCK];
146        aes.encrypt_block(&mut l_star);
147        // L_$ = double(L_*), L_0 = double(L_$)
148        let l_dollar = double(&l_star);
149        let l0 = double(&l_dollar);
150
151        Some(Self {
152            aes,
153            l_star,
154            l_dollar,
155            l: vec![l0],
156        })
157    }
158
159    /// Ensure `self.l` has at least index `i` (0-based), extending by
160    /// repeated doubling. `l[i]` is used when `ntz(block_index) == i`.
161    fn ensure_l(&mut self, i: usize) {
162        while self.l.len() <= i {
163            let last = *self.l.last().unwrap();
164            self.l.push(double(&last));
165        }
166    }
167
168    /// Compute the initial offset `Offset_0` from the nonce
169    /// (RFC 7253 §4.2), fixed `TAGLEN = 128`.
170    ///
171    /// `nonce.len()` must be 1..=15.
172    fn init_offset(&self, nonce: &[u8]) -> [u8; BLOCK] {
173        debug_assert!((1..=15).contains(&nonce.len()));
174
175        // Nonce block: top byte = TAGLEN mod 128 = 0 (128 mod 128),
176        // then zero pad, a single 1 bit before the nonce, then N.
177        let mut nb = [0u8; BLOCK];
178        // num2str(128 mod 128, 7) = 0, so byte 0 keeps its high 7 bits
179        // clear. The "1" bit sits at position 120 - 8*len from the MSB,
180        // i.e. just before the nonce.
181        nb[BLOCK - 1 - nonce.len()] = 0x01;
182        nb[BLOCK - nonce.len()..].copy_from_slice(nonce);
183
184        // bottom = last 6 bits of the nonce block.
185        let bottom = (nb[BLOCK - 1] & 0x3f) as usize;
186
187        // Ktop = ENCIPHER(K, Nonce with bottom 6 bits zeroed).
188        let mut ktop_in = nb;
189        ktop_in[BLOCK - 1] &= 0xc0;
190        let mut ktop = ktop_in;
191        self.aes.encrypt_block(&mut ktop);
192
193        // Stretch = Ktop ‖ (Ktop[0..8] xor Ktop[1..9]) — 24 bytes.
194        let mut stretch = [0u8; BLOCK + 8];
195        stretch[..BLOCK].copy_from_slice(&ktop);
196        for i in 0..8 {
197            stretch[BLOCK + i] = ktop[i] ^ ktop[i + 1];
198        }
199
200        // Offset_0 = Stretch[1+bottom .. 1+bottom+128] (bit-indexed).
201        // Extract 128 bits starting at bit `bottom` from the byte
202        // stream `stretch`.
203        let mut offset = [0u8; BLOCK];
204        let byte_shift = bottom / 8;
205        let bit_shift = (bottom % 8) as u32;
206        if bit_shift == 0 {
207            offset.copy_from_slice(&stretch[byte_shift..byte_shift + BLOCK]);
208        } else {
209            for i in 0..BLOCK {
210                let hi = (stretch[byte_shift + i] as u32) << bit_shift;
211                let lo = (stretch[byte_shift + i + 1] as u32) >> (8 - bit_shift);
212                offset[i] = (hi | lo) as u8;
213            }
214        }
215        offset
216    }
217
218    /// PMAC-style associated-data hash `HASH(K, A)` (RFC 7253 §4.1).
219    ///
220    /// Returns a 16-byte value that is xored into the tag. Uses `L_i`
221    /// doubling with `ntz(i)` exactly like the message path, but with
222    /// its own zero-initialized offset and sum.
223    fn hash_aad(&mut self, aad: &[u8]) -> [u8; BLOCK] {
224        let mut offset = [0u8; BLOCK];
225        let mut sum = [0u8; BLOCK];
226
227        let full = aad.len() / BLOCK;
228        for i in 0..full {
229            self.ensure_l(ntz(i + 1) as usize);
230            xor_block(&mut offset, &self.l[ntz(i + 1) as usize]);
231
232            let mut blk = [0u8; BLOCK];
233            blk.copy_from_slice(&aad[i * BLOCK..i * BLOCK + BLOCK]);
234            let mut enc = xor_blocks(&blk, &offset);
235            self.aes.encrypt_block(&mut enc);
236            xor_block(&mut sum, &enc);
237        }
238
239        // Final partial block (if any).
240        let rem = aad.len() % BLOCK;
241        if rem != 0 {
242            xor_block(&mut offset, &self.l_star);
243            let mut blk = [0u8; BLOCK];
244            blk[..rem].copy_from_slice(&aad[full * BLOCK..]);
245            blk[rem] = 0x80; // 10* padding
246            let mut enc = xor_blocks(&blk, &offset);
247            self.aes.encrypt_block(&mut enc);
248            xor_block(&mut sum, &enc);
249        }
250
251        sum
252    }
253
254    /// Encrypt and authenticate. Returns `ciphertext ‖ tag`
255    /// (`out.len() == plaintext.len() + 16`).
256    ///
257    /// `nonce.len()` must be 1..=15; returns `None` otherwise.
258    pub fn encrypt(&mut self, nonce: &[u8], aad: &[u8], plaintext: &[u8]) -> Option<Vec<u8>> {
259        if !(1..=15).contains(&nonce.len()) {
260            return None;
261        }
262
263        let mut offset = self.init_offset(nonce);
264        let mut checksum = [0u8; BLOCK];
265        let mut out = Vec::with_capacity(plaintext.len() + TAG_LEN);
266
267        let full = plaintext.len() / BLOCK;
268        for i in 0..full {
269            self.ensure_l(ntz(i + 1) as usize);
270            xor_block(&mut offset, &self.l[ntz(i + 1) as usize]);
271
272            let mut blk = [0u8; BLOCK];
273            blk.copy_from_slice(&plaintext[i * BLOCK..i * BLOCK + BLOCK]);
274            // Checksum ^= P_i
275            xor_block(&mut checksum, &blk);
276            // C_i = Offset ^ ENCIPHER(K, P_i ^ Offset)
277            let mut enc = xor_blocks(&blk, &offset);
278            self.aes.encrypt_block(&mut enc);
279            xor_block(&mut enc, &offset);
280            out.extend_from_slice(&enc);
281        }
282
283        // Final partial block (if any).
284        let rem = plaintext.len() % BLOCK;
285        if rem != 0 {
286            xor_block(&mut offset, &self.l_star);
287            // Pad = ENCIPHER(K, Offset)
288            let mut pad = offset;
289            self.aes.encrypt_block(&mut pad);
290            // C_* = P_* ^ Pad[0..rem]
291            let p_last = &plaintext[full * BLOCK..];
292            for i in 0..rem {
293                out.push(p_last[i] ^ pad[i]);
294            }
295            // Checksum ^= (P_* ‖ 1 ‖ 0*)
296            let mut cs_block = [0u8; BLOCK];
297            cs_block[..rem].copy_from_slice(p_last);
298            cs_block[rem] = 0x80;
299            xor_block(&mut checksum, &cs_block);
300        }
301
302        // Tag = ENCIPHER(K, Checksum ^ Offset ^ L_$) ^ HASH(K, A)
303        let mut tag_in = xor_blocks(&checksum, &offset);
304        xor_block(&mut tag_in, &self.l_dollar);
305        let mut tag = tag_in;
306        self.aes.encrypt_block(&mut tag);
307        let aad_hash = self.hash_aad(aad);
308        xor_block(&mut tag, &aad_hash);
309
310        out.extend_from_slice(&tag);
311        Some(out)
312    }
313
314    /// Decrypt and verify. `input` is `ciphertext ‖ tag`. Returns
315    /// `Some(plaintext)` only if the 16-byte tag verifies (constant-
316    /// time compare); returns `None` on any malformed input or tag
317    /// mismatch, and never releases plaintext on failure.
318    ///
319    /// `nonce.len()` must be 1..=15.
320    pub fn decrypt(&mut self, nonce: &[u8], aad: &[u8], input: &[u8]) -> Option<Vec<u8>> {
321        if !(1..=15).contains(&nonce.len()) {
322            return None;
323        }
324        if input.len() < TAG_LEN {
325            return None;
326        }
327        let (ciphertext, recv_tag) = input.split_at(input.len() - TAG_LEN);
328
329        let mut offset = self.init_offset(nonce);
330        let mut checksum = [0u8; BLOCK];
331        let mut plaintext = Vec::with_capacity(ciphertext.len());
332
333        let full = ciphertext.len() / BLOCK;
334        for i in 0..full {
335            self.ensure_l(ntz(i + 1) as usize);
336            xor_block(&mut offset, &self.l[ntz(i + 1) as usize]);
337
338            let mut blk = [0u8; BLOCK];
339            blk.copy_from_slice(&ciphertext[i * BLOCK..i * BLOCK + BLOCK]);
340            // P_i = Offset ^ DECIPHER(K, C_i ^ Offset)
341            let mut dec = xor_blocks(&blk, &offset);
342            self.aes.decrypt_block(&mut dec);
343            xor_block(&mut dec, &offset);
344            xor_block(&mut checksum, &dec);
345            plaintext.extend_from_slice(&dec);
346        }
347
348        // Final partial block (if any).
349        let rem = ciphertext.len() % BLOCK;
350        if rem != 0 {
351            xor_block(&mut offset, &self.l_star);
352            let mut pad = offset;
353            self.aes.encrypt_block(&mut pad); // OCB decrypt uses ENCIPHER for the pad
354            let c_last = &ciphertext[full * BLOCK..];
355            let mut p_last = [0u8; BLOCK];
356            for i in 0..rem {
357                p_last[i] = c_last[i] ^ pad[i];
358                plaintext.push(p_last[i]);
359            }
360            let mut cs_block = [0u8; BLOCK];
361            cs_block[..rem].copy_from_slice(&p_last[..rem]);
362            cs_block[rem] = 0x80;
363            xor_block(&mut checksum, &cs_block);
364        }
365
366        // Recompute the tag.
367        let mut tag_in = xor_blocks(&checksum, &offset);
368        xor_block(&mut tag_in, &self.l_dollar);
369        let mut tag = tag_in;
370        self.aes.encrypt_block(&mut tag);
371        let aad_hash = self.hash_aad(aad);
372        xor_block(&mut tag, &aad_hash);
373
374        // Constant-time tag compare; drop plaintext on failure so a
375        // wrong tag never releases it.
376        if silentops::ct_eq(&tag, recv_tag) == 1 {
377            Some(plaintext)
378        } else {
379            None
380        }
381    }
382}
383
384// ============================================================================
385// Tests (RFC 7253 Appendix A pinned vectors)
386// ============================================================================
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    fn hex(s: &str) -> Vec<u8> {
393        let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
394        assert!(s.len() % 2 == 0);
395        (0..s.len())
396            .step_by(2)
397            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
398            .collect()
399    }
400
401    /// RFC 7253 Appendix A sample-results key (AES-128) and nonce stem.
402    /// `K = 000102...0F`, nonce = `BBAA99887766554433221100` with the
403    /// low byte incremented per case.
404    const KEY128: &str = "000102030405060708090A0B0C0D0E0F";
405
406    fn key128() -> Vec<u8> {
407        hex(KEY128)
408    }
409
410    /// RFC 7253 Appendix A test-vector case: given AAD, plaintext, and
411    /// expected `ciphertext ‖ tag`, assert encrypt matches and decrypt
412    /// round-trips.
413    fn check_case(nonce_hex: &str, aad_hex: &str, pt_hex: &str, expected_hex: &str) {
414        let key = key128();
415        let nonce = hex(nonce_hex);
416        let aad = hex(aad_hex);
417        let pt = hex(pt_hex);
418        let expected = hex(expected_hex);
419
420        let mut ocb = Ocb::new(&key).unwrap();
421        let out = ocb.encrypt(&nonce, &aad, &pt).unwrap();
422        assert_eq!(out, expected, "OCB encrypt mismatch (nonce {nonce_hex})");
423
424        let mut ocb2 = Ocb::new(&key).unwrap();
425        let back = ocb2.decrypt(&nonce, &aad, &out).unwrap();
426        assert_eq!(back, pt, "OCB decrypt mismatch (nonce {nonce_hex})");
427    }
428
429    /// RFC 7253 Appendix A case 1 — empty plaintext, empty AAD.
430    #[test]
431    fn rfc7253_case1_empty() {
432        check_case("BBAA99887766554433221100", "", "", "785407BFFFC8AD9EDCC5520AC9111EE6");
433    }
434
435    /// RFC 7253 Appendix A case 2 — one AAD block, one plaintext block.
436    #[test]
437    fn rfc7253_case2_one_block() {
438        check_case(
439            "BBAA99887766554433221101",
440            "0001020304050607",
441            "0001020304050607",
442            "6820B3657B6F615A5725BDA0D3B4EB3A257C9AF1F8F03009",
443        );
444    }
445
446    /// RFC 7253 Appendix A case 3 — AAD only (empty plaintext).
447    #[test]
448    fn rfc7253_case3_aad_only() {
449        check_case(
450            "BBAA99887766554433221102",
451            "0001020304050607",
452            "",
453            "81017F8203F081277152FADE694A0A00",
454        );
455    }
456
457    /// RFC 7253 Appendix A case 4 — plaintext with no AAD.
458    #[test]
459    fn rfc7253_case4_pt_no_aad() {
460        check_case(
461            "BBAA99887766554433221103",
462            "",
463            "0001020304050607",
464            "45DD69F8F5AAE72414054CD1F35D82760B2CD00D2F99BFA9",
465        );
466    }
467
468    /// RFC 7253 Appendix A case 5 — one full block of AAD and plaintext.
469    #[test]
470    fn rfc7253_case5_full_block() {
471        check_case(
472            "BBAA99887766554433221104",
473            "000102030405060708090A0B0C0D0E0F",
474            "000102030405060708090A0B0C0D0E0F",
475            "571D535B60B277188BE5147170A9A22C3AD7A4FF3835B8C5701C1CCEC8FC3358",
476        );
477    }
478
479    /// RFC 7253 Appendix A case 7 — one full block of plaintext, no AAD.
480    #[test]
481    fn rfc7253_case7_pt_no_aad_full() {
482        check_case(
483            "BBAA99887766554433221106",
484            "",
485            "000102030405060708090A0B0C0D0E0F",
486            "5CE88EC2E0692706A915C00AEB8B2396F40E1C743F52436BDF06D8FA1ECA343D",
487        );
488    }
489
490    /// RFC 7253 Appendix A case 10 — 24-byte plaintext, no AAD (checks
491    /// the L_i doubling cache across multiple blocks).
492    #[test]
493    fn rfc7253_case10_three_blocks() {
494        check_case(
495            "BBAA99887766554433221109",
496            "",
497            "000102030405060708090A0B0C0D0E0F1011121314151617",
498            "221BD0DE7FA6FE993ECCD769460A0AF2D6CDED0C395B1C3CE725F32494B9F914D85C0B1EB38357FF",
499        );
500    }
501
502    /// RFC 7253 Appendix A case 14 — 40-byte (2.5 block) plaintext +
503    /// 40-byte AAD, exercising both final partial-block paths.
504    #[test]
505    fn rfc7253_case14_partial_block() {
506        check_case(
507            "BBAA9988776655443322110D",
508            "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627",
509            "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627",
510            "D5CA91748410C1751FF8A2F618255B68A0A12E093FF454606E59F9C1D0DDC54B65E8628E568BAD7AED07BA06A4A69483A7035490C5769E60",
511        );
512    }
513
514    /// RFC 7253 Appendix A case 16 — 40-byte plaintext, no AAD.
515    #[test]
516    fn rfc7253_case16_partial_no_aad() {
517        check_case(
518            "BBAA9988776655443322110F",
519            "",
520            "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627",
521            "4412923493C57D5DE0D700F753CCE0D1D2D95060122E9F15A5DDBFC5787E50B5CC55EE507BCB084E479AD363AC366B95A98CA5F3000B1479",
522        );
523    }
524
525    /// Tampered tag must be rejected (constant-time compare) and no
526    /// plaintext returned.
527    #[test]
528    fn ocb_rejects_tampered_tag() {
529        let key = key128();
530        let nonce = hex("BBAA99887766554433221103");
531        let pt = hex("000102030405060708090A0B0C0D0E0F");
532
533        let mut ocb = Ocb::new(&key).unwrap();
534        let mut out = ocb.encrypt(&nonce, b"", &pt).unwrap();
535        let last = out.len() - 1;
536        out[last] ^= 0x01;
537
538        let mut ocb2 = Ocb::new(&key).unwrap();
539        assert!(ocb2.decrypt(&nonce, b"", &out).is_none());
540    }
541
542    /// Tampered ciphertext must be rejected.
543    #[test]
544    fn ocb_rejects_tampered_ciphertext() {
545        let key = key128();
546        let nonce = hex("BBAA99887766554433221103");
547        let pt = hex("000102030405060708090A0B0C0D0E0F");
548
549        let mut ocb = Ocb::new(&key).unwrap();
550        let mut out = ocb.encrypt(&nonce, b"", &pt).unwrap();
551        out[0] ^= 0x01;
552
553        let mut ocb2 = Ocb::new(&key).unwrap();
554        assert!(ocb2.decrypt(&nonce, b"", &out).is_none());
555    }
556
557    /// Modified AAD must be detected.
558    #[test]
559    fn ocb_rejects_modified_aad() {
560        let key = key128();
561        let nonce = hex("BBAA99887766554433221101");
562        let pt = hex("0001020304050607");
563
564        let mut ocb = Ocb::new(&key).unwrap();
565        let out = ocb.encrypt(&nonce, &hex("0001020304050607"), &pt).unwrap();
566
567        let mut ocb2 = Ocb::new(&key).unwrap();
568        assert!(ocb2.decrypt(&nonce, &hex("0001020304050608"), &out).is_none());
569    }
570
571    /// AES-192 and AES-256 round-trip (RFC allows any AES width; the
572    /// Appendix A per-case vectors are AES-128 only, so we pin round-trip
573    /// here — the per-width *tags* are pinned by the summary KAT below).
574    #[test]
575    fn ocb_aes192_aes256_roundtrip() {
576        for klen in [24usize, 32] {
577            let key: Vec<u8> = (0..klen).map(|i| i as u8).collect();
578            let nonce = hex("BBAA99887766554433221103");
579            let aad = b"associated data";
580            let pt = b"a message that spans more than a couple of AES blocks for good measure!!";
581
582            let mut ocb = Ocb::new(&key).unwrap();
583            let out = ocb.encrypt(&nonce, aad, pt).unwrap();
584            let mut ocb2 = Ocb::new(&key).unwrap();
585            let back = ocb2.decrypt(&nonce, aad, &out).unwrap();
586            assert_eq!(back.as_slice(), pt.as_slice(), "AES-{} OCB round-trip", klen * 8);
587        }
588    }
589
590    // ------------------------------------------------------------------
591    // RFC 7253 Appendix A summary KAT (all three key widths + high ntz).
592    //
593    // Provenance: RFC 7253 §Appendix A, "The following algorithm tests a
594    // wider variety of inputs." (canonical text:
595    // https://www.rfc-editor.org/rfc/rfc7253.txt, IETF May 2014).
596    //
597    // The 128-iteration loop drives OCB-ENCRYPT over many message/AAD
598    // lengths, then a final empty-plaintext OCB-ENCRYPT over the whole
599    // accumulated ciphertext C authenticates it. With TAGLEN = 128 the
600    // final output is exactly the 16-byte tag. This exercises all three
601    // AES widths AND deep L_i doubling (the concatenated C reaches ~22 kB,
602    // so single messages inside the final call span well over 128 blocks →
603    // high `ntz` in the L_i cache).
604    //
605    // NOTE (finding): the AES-192 tag is F673F2C3E7174AAE7BAE986CA9F29E17
606    // per the RFC text (line 997) — cross-checked against the canonical
607    // rfc-editor copy, not a paraphrase.
608
609    /// Big-endian 96-bit (12-byte) nonce `num2str(v, 96)`.
610    fn num2str96(v: u32) -> [u8; 12] {
611        let mut n = [0u8; 12];
612        n[8..].copy_from_slice(&v.to_be_bytes());
613        n
614    }
615
616    /// Run the RFC 7253 Appendix A summary algorithm for one key length
617    /// and return the 16-byte tag (`Output`). `TAGLEN = 128`, so
618    /// `K = zeros(KEYLEN-8) || num2str(128, 8)` → last key byte 0x80.
619    fn ocb_summary_kat(keylen: usize) -> Vec<u8> {
620        let mut key = vec![0u8; keylen];
621        *key.last_mut().unwrap() = 128u8; // num2str(TAGLEN=128, 8)
622
623        let mut c: Vec<u8> = Vec::new();
624        for i in 0u32..128 {
625            // S = zeros(8i) — 8i *bits*, i.e. i zero bytes.
626            let s = vec![0u8; i as usize];
627
628            let mut ocb = Ocb::new(&key).unwrap();
629            let n = num2str96(3 * i + 1);
630            c.extend_from_slice(&ocb.encrypt(&n, &s, &s).unwrap());
631
632            let mut ocb = Ocb::new(&key).unwrap();
633            let n = num2str96(3 * i + 2);
634            c.extend_from_slice(&ocb.encrypt(&n, &[], &s).unwrap());
635
636            let mut ocb = Ocb::new(&key).unwrap();
637            let n = num2str96(3 * i + 3);
638            c.extend_from_slice(&ocb.encrypt(&n, &s, &[]).unwrap());
639        }
640
641        // Output = OCB-ENCRYPT(K, num2str(385,96), C, <empty>) → tag only.
642        let mut ocb = Ocb::new(&key).unwrap();
643        let n = num2str96(385);
644        ocb.encrypt(&n, &c, &[]).unwrap()
645    }
646
647    #[test]
648    fn rfc7253_appendix_a_summary_aes128() {
649        assert_eq!(ocb_summary_kat(16), hex("67E944D23256C5E0B6C61FA22FDF1EA2"));
650    }
651
652    #[test]
653    fn rfc7253_appendix_a_summary_aes192() {
654        assert_eq!(ocb_summary_kat(24), hex("F673F2C3E7174AAE7BAE986CA9F29E17"));
655    }
656
657    #[test]
658    fn rfc7253_appendix_a_summary_aes256() {
659        assert_eq!(ocb_summary_kat(32), hex("D90EB8E9C977C88B79DD793D7FFA161C"));
660    }
661
662    /// Large single-message (> 128 blocks) AES-256 encrypt/decrypt
663    /// round-trip. Directly exercises high `ntz(i)` in the `L_i` doubling
664    /// cache within one message (the summary KAT reaches high ntz across a
665    /// multi-kB final call; this pins it for a single call under AES-256).
666    #[test]
667    fn ocb_aes256_large_message_high_ntz() {
668        let key: Vec<u8> = (0..32u8).collect();
669        let nonce = hex("BBAA99887766554433221103");
670        let aad = b"high-ntz associated data";
671        // 200 full 16-byte blocks + a 7-byte tail: ntz reaches 7 (block 128).
672        let pt: Vec<u8> = (0..(200 * 16 + 7)).map(|i| (i * 7 + 3) as u8).collect();
673
674        let mut ocb = Ocb::new(&key).unwrap();
675        let out = ocb.encrypt(&nonce, aad, &pt).unwrap();
676        assert_eq!(out.len(), pt.len() + TAG_LEN);
677        let mut ocb2 = Ocb::new(&key).unwrap();
678        let back = ocb2.decrypt(&nonce, aad, &out).unwrap();
679        assert_eq!(back, pt, "AES-256 OCB high-ntz round-trip");
680    }
681
682    /// Invalid key / nonce lengths rejected.
683    #[test]
684    fn ocb_rejects_bad_params() {
685        assert!(Ocb::new(&[0u8; 17]).is_none());
686        let mut ocb = Ocb::new(&key128()).unwrap();
687        assert!(ocb.encrypt(&[], b"", b"").is_none()); // empty nonce
688        assert!(ocb.encrypt(&[0u8; 16], b"", b"").is_none()); // 16-byte nonce
689        assert!(ocb.decrypt(&hex("BBAA99887766554433221100"), b"", &[0u8; 8]).is_none()); // short input
690    }
691}