mumble_server_runtime_crypto/
ocb2.rs

1//! OCB2-AES128 as implemented by Mumble, including its mitigations against the
2//! attack in <https://eprint.iacr.org/2019/311> (section 9 / XEX* forgery).
3//!
4//! Ported from the vendored reference (R1); every routine mirrors its C++ source:
5//!
6//! REF: references/vendored/ocb2-vectors/CryptStateOCB2.cpp : `ocb_encrypt`,
7//!      `ocb_decrypt`, `encrypt`, `decrypt`, `S2`, `S3`, `XOR`.
8//! REF: references/vendored/ocb2-vectors/TestCrypt.cpp : the golden vectors and
9//!      mitigation tests reproduced in this module's `tests`.
10//!
11//! The block "doubling" (`S2`) and "tripling" (`S3`) treat the 16-byte block as a
12//! big-endian 128-bit element of GF(2^128) with reduction polynomial 0x87; the
13//! reference's endianness dance (`SWAPPED`) is just that operation, written here
14//! portably in terms of bytes.
15
16use aes::Aes128;
17use aes::cipher::generic_array::GenericArray;
18use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
19
20/// AES-128 block size in bytes.
21pub const BLOCK_SIZE: usize = 16;
22/// AES-128 key size in bytes.
23pub const KEY_SIZE: usize = 16;
24
25/// A single 16-byte block.
26type Block = [u8; BLOCK_SIZE];
27
28/// XOR two blocks.
29/// REF: CryptStateOCB2.cpp : `XOR`.
30fn xor(a: &Block, b: &Block) -> Block {
31    let mut out = [0u8; BLOCK_SIZE];
32    for i in 0..BLOCK_SIZE {
33        out[i] = a[i] ^ b[i];
34    }
35    out
36}
37
38/// Double a block in GF(2^128): left-shift the big-endian 128-bit value by one
39/// bit, reducing with 0x87 when the top bit was set.
40/// REF: CryptStateOCB2.cpp : `S2`.
41fn times2(block: &mut Block) {
42    let carry = block[0] >> 7;
43    for i in 0..BLOCK_SIZE - 1 {
44        block[i] = (block[i] << 1) | (block[i + 1] >> 7);
45    }
46    block[BLOCK_SIZE - 1] = (block[BLOCK_SIZE - 1] << 1) ^ (carry * 0x87);
47}
48
49/// Triple a block in GF(2^128): `3*x == x ^ 2*x`.
50/// REF: CryptStateOCB2.cpp : `S3` (`block ^= <double>`).
51fn times3(block: &mut Block) {
52    let mut doubled = *block;
53    times2(&mut doubled);
54    for i in 0..BLOCK_SIZE {
55        block[i] ^= doubled[i];
56    }
57}
58
59/// AES-128 encrypt one block.
60fn aes_encrypt(cipher: &Aes128, input: &Block) -> Block {
61    let mut buffer = GenericArray::clone_from_slice(input);
62    cipher.encrypt_block(&mut buffer);
63    let mut out = [0u8; BLOCK_SIZE];
64    out.copy_from_slice(buffer.as_slice());
65    out
66}
67
68/// AES-128 decrypt one block.
69fn aes_decrypt(cipher: &Aes128, input: &Block) -> Block {
70    let mut buffer = GenericArray::clone_from_slice(input);
71    cipher.decrypt_block(&mut buffer);
72    let mut out = [0u8; BLOCK_SIZE];
73    out.copy_from_slice(buffer.as_slice());
74    out
75}
76
77/// The 16-byte block encoding the final-block bit length (`len * 8`), big-endian.
78/// REF: CryptStateOCB2.cpp : `tmp[BLOCKSIZE - 1] = SWAPPED(len * 8)`.
79fn length_block(len: usize) -> Block {
80    // Widening usize -> u128 is lossless and `* 8` cannot overflow u128 for any
81    // real length; the fallback is unreachable and kept only to avoid `as`.
82    let bits = u128::try_from(len).unwrap_or(u128::MAX).wrapping_mul(8);
83    bits.to_be_bytes()
84}
85
86/// OCB2 encryption of `plain` under `cipher` with the given `nonce`.
87///
88/// Returns `(authentic, ciphertext, tag)`. `authentic` is false only when a
89/// XEX*-critical block is detected and `modify_on_xexstar_attack` is false — the
90/// path used only to exercise [`ocb_decrypt`]'s detection. With the flag true
91/// (the production setting), a critical block has a bit flipped so encryption
92/// stays authentic while defeating the attack.
93///
94/// REF: CryptStateOCB2.cpp : `ocb_encrypt`.
95pub fn ocb_encrypt(
96    cipher: &Aes128,
97    plain: &[u8],
98    nonce: &Block,
99    modify_on_xexstar_attack: bool,
100) -> (bool, Vec<u8>, Block) {
101    let mut authentic = true;
102    let mut delta = aes_encrypt(cipher, nonce);
103    let mut checksum = [0u8; BLOCK_SIZE];
104    let mut encrypted = Vec::with_capacity(plain.len());
105
106    let mut offset = 0;
107    let mut remaining = plain.len();
108    while remaining > BLOCK_SIZE {
109        let block = read_block(plain, offset);
110
111        // Counter-cryptanalysis (section 9 of eprint 2019/311): the second-to-last
112        // block being all zero except possibly its last byte is XEX*-critical.
113        let mut flip_a_bit = false;
114        if remaining - BLOCK_SIZE <= BLOCK_SIZE {
115            let mut sum = 0u8;
116            for &byte in &block[..BLOCK_SIZE - 1] {
117                sum |= byte;
118            }
119            if sum == 0 {
120                if modify_on_xexstar_attack {
121                    flip_a_bit = true;
122                } else {
123                    authentic = false;
124                }
125            }
126        }
127
128        times2(&mut delta);
129        let mut tmp = xor(&delta, &block);
130        if flip_a_bit {
131            tmp[0] ^= 1;
132        }
133        tmp = aes_encrypt(cipher, &tmp);
134        encrypted.extend_from_slice(&xor(&delta, &tmp));
135
136        checksum = xor(&checksum, &block);
137        if flip_a_bit {
138            checksum[0] ^= 1;
139        }
140
141        offset += BLOCK_SIZE;
142        remaining -= BLOCK_SIZE;
143    }
144
145    // Final (possibly partial) block.
146    times2(&mut delta);
147    let pad = aes_encrypt(cipher, &xor(&length_block(remaining), &delta));
148
149    let mut tail = pad;
150    tail[..remaining].copy_from_slice(&plain[offset..offset + remaining]);
151    checksum = xor(&checksum, &tail);
152    let out_tail = xor(&pad, &tail);
153    encrypted.extend_from_slice(&out_tail[..remaining]);
154
155    times3(&mut delta);
156    let tag = aes_encrypt(cipher, &xor(&delta, &checksum));
157
158    (authentic, encrypted, tag)
159}
160
161/// OCB2 decryption of `encrypted` under `cipher` with the given `nonce`.
162///
163/// Returns `(authentic, plaintext, tag)`. The caller must still compare `tag`
164/// against the transmitted tag; `authentic` additionally goes false when the
165/// decrypted final block matches the XEX*-critical shape, which no honest packet
166/// produces.
167///
168/// REF: CryptStateOCB2.cpp : `ocb_decrypt`.
169pub fn ocb_decrypt(cipher: &Aes128, encrypted: &[u8], nonce: &Block) -> (bool, Vec<u8>, Block) {
170    let mut authentic = true;
171    let mut delta = aes_encrypt(cipher, nonce);
172    let mut checksum = [0u8; BLOCK_SIZE];
173    let mut plain = Vec::with_capacity(encrypted.len());
174
175    let mut offset = 0;
176    let mut remaining = encrypted.len();
177    while remaining > BLOCK_SIZE {
178        let block = read_block(encrypted, offset);
179        times2(&mut delta);
180        let tmp = aes_decrypt(cipher, &xor(&delta, &block));
181        let decoded = xor(&delta, &tmp);
182        plain.extend_from_slice(&decoded);
183        checksum = xor(&checksum, &decoded);
184        offset += BLOCK_SIZE;
185        remaining -= BLOCK_SIZE;
186    }
187
188    // Final (possibly partial) block.
189    times2(&mut delta);
190    let pad = aes_encrypt(cipher, &xor(&length_block(remaining), &delta));
191
192    let mut tail = [0u8; BLOCK_SIZE];
193    tail[..remaining].copy_from_slice(&encrypted[offset..offset + remaining]);
194    tail = xor(&tail, &pad);
195    checksum = xor(&checksum, &tail);
196    plain.extend_from_slice(&tail[..remaining]);
197
198    // XEX* forgery detection: an attack needs the decrypted last block to equal
199    // `delta` in every byte the length field does not touch (all but the last).
200    if tail[..BLOCK_SIZE - 1] == delta[..BLOCK_SIZE - 1] {
201        authentic = false;
202    }
203
204    times3(&mut delta);
205    let tag = aes_encrypt(cipher, &xor(&delta, &checksum));
206
207    (authentic, plain, tag)
208}
209
210/// Read the 16-byte block at `offset`. The callers only reach this with at least
211/// a full block available, but it is written to never index out of bounds.
212fn read_block(data: &[u8], offset: usize) -> Block {
213    let mut block = [0u8; BLOCK_SIZE];
214    if let Some(slice) = data.get(offset..offset + BLOCK_SIZE) {
215        block.copy_from_slice(slice);
216    }
217    block
218}
219
220/// A full OCB2 crypt state for one direction pair: the AES key, both IVs, the
221/// replay history and the good/late/lost counters. Mirrors Mumble's
222/// `CryptStateOCB2` object (minus key generation, which belongs to session setup).
223///
224/// REF: CryptStateOCB2.cpp : `CryptStateOCB2`, `encrypt`, `decrypt`.
225pub struct CryptState {
226    cipher: Aes128,
227    encrypt_iv: Block,
228    decrypt_iv: Block,
229    decrypt_history: [u8; 256],
230    /// Count of successfully decrypted packets.
231    pub good: u32,
232    /// Running count of late (out-of-order but recovered) packets.
233    pub late: u32,
234    /// Running count of lost packets inferred from IV gaps.
235    pub lost: u32,
236}
237
238impl CryptState {
239    /// Build a state from a raw key and the two initial IVs.
240    pub fn new(raw_key: &[u8; KEY_SIZE], encrypt_iv: &Block, decrypt_iv: &Block) -> Self {
241        Self {
242            cipher: Aes128::new(GenericArray::from_slice(raw_key)),
243            encrypt_iv: *encrypt_iv,
244            decrypt_iv: *decrypt_iv,
245            decrypt_history: [0u8; 256],
246            good: 0,
247            late: 0,
248            lost: 0,
249        }
250    }
251
252    /// Current encrypt IV (mainly for tests that force/inspect IV state).
253    pub fn encrypt_iv(&self) -> Block {
254        self.encrypt_iv
255    }
256
257    /// Current decrypt IV.
258    pub fn decrypt_iv(&self) -> Block {
259        self.decrypt_iv
260    }
261
262    /// Overwrite the encrypt IV.
263    pub fn set_encrypt_iv(&mut self, iv: &Block) {
264        self.encrypt_iv = *iv;
265    }
266
267    /// Overwrite the decrypt IV.
268    pub fn set_decrypt_iv(&mut self, iv: &Block) {
269        self.decrypt_iv = *iv;
270    }
271
272    /// Encrypt a packet: advance the IV, OCB2-encrypt, and prepend the 4-byte
273    /// header (IV low byte plus three tag bytes). Returns `None` only if
274    /// encryption reports inauthenticity, which cannot happen here because the
275    /// XEX* mitigation is enabled.
276    ///
277    /// REF: CryptStateOCB2.cpp : `CryptStateOCB2::encrypt`.
278    pub fn encrypt(&mut self, plain: &[u8]) -> Option<Vec<u8>> {
279        increment_iv(&mut self.encrypt_iv);
280        let (authentic, ciphertext, tag) = ocb_encrypt(&self.cipher, plain, &self.encrypt_iv, true);
281        if !authentic {
282            return None;
283        }
284        let mut packet = Vec::with_capacity(4 + ciphertext.len());
285        packet.push(self.encrypt_iv[0]);
286        packet.extend_from_slice(&tag[..3]);
287        packet.extend_from_slice(&ciphertext);
288        Some(packet)
289    }
290
291    /// Decrypt a packet, performing IV recovery for out-of-order/lost packets and
292    /// rejecting replays. Returns the plaintext, or `None` if the packet is a
293    /// replay, fails IV recovery, or fails tag verification (fail closed, R6).
294    ///
295    /// REF: CryptStateOCB2.cpp : `CryptStateOCB2::decrypt`.
296    pub fn decrypt(&mut self, source: &[u8]) -> Option<Vec<u8>> {
297        if source.len() < 4 {
298            return None;
299        }
300        let (header, ciphertext) = source.split_at(4);
301        let ivbyte = header[0];
302        let saveiv = self.decrypt_iv;
303        let mut restore = false;
304        let mut late: i32 = 0;
305        let mut lost: i32 = 0;
306
307        if self.decrypt_iv[0].wrapping_add(1) == ivbyte {
308            // In order as expected.
309            if ivbyte > self.decrypt_iv[0] {
310                self.decrypt_iv[0] = ivbyte;
311            } else if ivbyte < self.decrypt_iv[0] {
312                self.decrypt_iv[0] = ivbyte;
313                increment_iv_from(&mut self.decrypt_iv, 1);
314            } else {
315                return None;
316            }
317        } else {
318            // Either out of order or a repeat.
319            let mut diff = ivbyte as i32 - self.decrypt_iv[0] as i32;
320            if diff > 128 {
321                diff -= 256;
322            } else if diff < -128 {
323                diff += 256;
324            }
325
326            if ivbyte < self.decrypt_iv[0] && diff > -30 && diff < 0 {
327                // Late packet, but no wraparound.
328                late = 1;
329                lost = -1;
330                self.decrypt_iv[0] = ivbyte;
331                restore = true;
332            } else if ivbyte > self.decrypt_iv[0] && diff > -30 && diff < 0 {
333                // Last was e.g. 0x02, here comes 0xff from the previous round.
334                late = 1;
335                lost = -1;
336                self.decrypt_iv[0] = ivbyte;
337                decrement_iv_from(&mut self.decrypt_iv, 1);
338                restore = true;
339            } else if ivbyte > self.decrypt_iv[0] && diff > 0 {
340                // Lost a few packets, but beyond that we are good.
341                lost = ivbyte as i32 - self.decrypt_iv[0] as i32 - 1;
342                self.decrypt_iv[0] = ivbyte;
343            } else if ivbyte < self.decrypt_iv[0] && diff > 0 {
344                // Lost a few packets, and wrapped around.
345                lost = 256 - self.decrypt_iv[0] as i32 + ivbyte as i32 - 1;
346                self.decrypt_iv[0] = ivbyte;
347                increment_iv_from(&mut self.decrypt_iv, 1);
348            } else {
349                return None;
350            }
351
352            if self.decrypt_history[self.decrypt_iv[0] as usize] == self.decrypt_iv[1] {
353                self.decrypt_iv = saveiv;
354                return None;
355            }
356        }
357
358        let (authentic, plain, tag) = ocb_decrypt(&self.cipher, ciphertext, &self.decrypt_iv);
359        if !authentic || tag[..3] != header[1..4] {
360            self.decrypt_iv = saveiv;
361            return None;
362        }
363
364        self.decrypt_history[self.decrypt_iv[0] as usize] = self.decrypt_iv[1];
365        if restore {
366            self.decrypt_iv = saveiv;
367        }
368
369        self.good = self.good.wrapping_add(1);
370        apply_stat(&mut self.late, late);
371        apply_stat(&mut self.lost, lost);
372        Some(plain)
373    }
374}
375
376/// Increment the IV as a little-endian 128-bit counter (carry from byte 0 up).
377/// REF: CryptStateOCB2.cpp : `for (i) if (++encrypt_iv[i]) break;`.
378fn increment_iv(iv: &mut Block) {
379    increment_iv_from(iv, 0);
380}
381
382fn increment_iv_from(iv: &mut Block, start: usize) {
383    for byte in iv.iter_mut().skip(start) {
384        *byte = byte.wrapping_add(1);
385        if *byte != 0 {
386            break;
387        }
388    }
389}
390
391/// Borrow-decrement the IV from `start`, mirroring the reference post-decrement
392/// (`if (decrypt_iv[i]--) break;` breaks when the pre-decrement value was nonzero).
393fn decrement_iv_from(iv: &mut Block, start: usize) {
394    for byte in iv.iter_mut().skip(start) {
395        let was = *byte;
396        *byte = byte.wrapping_sub(1);
397        if was != 0 {
398            break;
399        }
400    }
401}
402
403/// Apply a signed delta to an unsigned counter without wrapping below zero, as
404/// the reference does for its late/lost statistics.
405/// REF: CryptStateOCB2.cpp : the `uiLate`/`uiLost` update at the end of `decrypt`.
406fn apply_stat(counter: &mut u32, delta: i32) {
407    if delta > 0 {
408        *counter = counter.wrapping_add(delta as u32);
409    } else if (*counter as i32) > delta.abs() {
410        *counter -= delta.unsigned_abs();
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    // Test asserts use expect() on Options; see mumble-server-runtime-protocol/framing.rs for
417    // why this is allowed under -D warnings.
418    #![allow(clippy::expect_used)]
419
420    use super::*;
421
422    /// The reference test key: bytes 0x00..0x0f.
423    /// REF: TestCrypt.cpp : `testvectors` / `authcrypt` `rawkey`.
424    const RAW_KEY: [u8; KEY_SIZE] = [
425        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
426        0x0f,
427    ];
428
429    fn cipher() -> Aes128 {
430        Aes128::new(GenericArray::from_slice(&RAW_KEY))
431    }
432
433    // REF: TestCrypt.cpp : TestCrypt::testvectors (from draft-krovetz-ocb-00).
434    #[test]
435    fn ocb_test_vectors_from_krovetz_draft() {
436        let cipher = cipher();
437
438        // Empty plaintext: tag is the "blank tag".
439        let (authentic, ciphertext, tag) = ocb_encrypt(&cipher, &[], &RAW_KEY, true);
440        assert!(authentic);
441        assert!(ciphertext.is_empty());
442        const BLANK_TAG: Block = [
443            0xBF, 0x31, 0x08, 0x13, 0x07, 0x73, 0xAD, 0x5E, 0xC7, 0x0E, 0xC6, 0x9E, 0x78, 0x75,
444            0xA7, 0xB0,
445        ];
446        assert_eq!(tag, BLANK_TAG);
447
448        // 40-byte plaintext 0x00..0x27.
449        let source: Vec<u8> = (0u8..40).collect();
450        let (authentic, ciphertext, tag) = ocb_encrypt(&cipher, &source, &RAW_KEY, true);
451        assert!(authentic);
452        const LONG_TAG: Block = [
453            0x9D, 0xB0, 0xCD, 0xF8, 0x80, 0xF7, 0x3E, 0x3E, 0x10, 0xD4, 0xEB, 0x32, 0x17, 0x76,
454            0x66, 0x88,
455        ];
456        const CRYPTED: [u8; 40] = [
457            0xF7, 0x5D, 0x6B, 0xC8, 0xB4, 0xDC, 0x8D, 0x66, 0xB8, 0x36, 0xA2, 0xB0, 0x8B, 0x32,
458            0xA6, 0x36, 0x9F, 0x1C, 0xD3, 0xC5, 0x22, 0x8D, 0x79, 0xFD, 0x6C, 0x26, 0x7F, 0x5F,
459            0x6A, 0xA7, 0xB2, 0x31, 0xC7, 0xDF, 0xB9, 0xD5, 0x99, 0x51, 0xAE, 0x9C,
460        ];
461        assert_eq!(tag, LONG_TAG);
462        assert_eq!(ciphertext, CRYPTED);
463    }
464
465    // REF: TestCrypt.cpp : TestCrypt::authcrypt.
466    #[test]
467    fn authcrypt_roundtrips_every_length_and_agrees_on_tag() {
468        let nonce: Block = [
469            0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22,
470            0x11, 0x00,
471        ];
472        let cipher = cipher();
473        for len in 0u32..128 {
474            let src: Vec<u8> = (0..len).map(|i| (i + 1) as u8).collect();
475            let (enc_ok, encrypted, enc_tag) = ocb_encrypt(&cipher, &src, &nonce, true);
476            let (dec_ok, decrypted, dec_tag) = ocb_decrypt(&cipher, &encrypted, &nonce);
477            assert!(enc_ok, "encrypt authentic at len {len}");
478            assert!(dec_ok, "decrypt authentic at len {len}");
479            assert_eq!(enc_tag, dec_tag, "tags agree at len {len}");
480            assert_eq!(decrypted, src, "roundtrip at len {len}");
481        }
482    }
483
484    // REF: TestCrypt.cpp : TestCrypt::xexstarAttack.
485    #[test]
486    fn xexstar_attack_is_detected() {
487        let nonce: Block = [
488            0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22,
489            0x11, 0x00,
490        ];
491        let cipher = cipher();
492
493        let mut src = vec![0u8; 2 * BLOCK_SIZE];
494        // First block: length of the second block, in bits.
495        src[BLOCK_SIZE - 1] = (BLOCK_SIZE * 8) as u8;
496        // Second block: arbitrary.
497        for b in src[BLOCK_SIZE..].iter_mut() {
498            *b = 42;
499        }
500
501        // Without the mitigation, encryption reports the critical block.
502        let (enc_ok, mut encrypted, mut enc_tag) = ocb_encrypt(&cipher, &src, &nonce, false);
503        let failed_encrypt = !enc_ok;
504
505        // Perform the forgery on the ciphertext.
506        encrypted[BLOCK_SIZE - 1] ^= (BLOCK_SIZE * 8) as u8;
507        for i in 0..BLOCK_SIZE {
508            enc_tag[i] = src[BLOCK_SIZE + i] ^ encrypted[BLOCK_SIZE + i];
509        }
510
511        // Decrypt just the first block: detection must fire.
512        let (dec_ok, _plain, dec_tag) = ocb_decrypt(&cipher, &encrypted[..BLOCK_SIZE], &nonce);
513        let failed_decrypt = !dec_ok;
514
515        // The forged tag matches (attack is correctly reproduced) ...
516        assert_eq!(enc_tag, dec_tag);
517        // ... and both sides flag it.
518        assert!(failed_encrypt);
519        assert!(failed_decrypt);
520
521        // With the mitigation on, the same plaintext encrypts and decrypts
522        // authentically, and the critical first block is altered (0 -> 1).
523        let (enc_ok, encrypted, enc_tag) = ocb_encrypt(&cipher, &src, &nonce, true);
524        let (dec_ok, decrypted, dec_tag) = ocb_decrypt(&cipher, &encrypted, &nonce);
525        assert!(enc_ok);
526        assert!(dec_ok);
527        assert_eq!(enc_tag, dec_tag);
528        assert_eq!(src[0], 0);
529        assert_eq!(decrypted[0], 1);
530    }
531
532    // REF: TestCrypt.cpp : TestCrypt::tamper.
533    #[test]
534    fn tamper_with_any_bit_is_rejected() {
535        let nonce: Block = [
536            0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22,
537            0x11, 0x00,
538        ];
539        let mut state = CryptState::new(&RAW_KEY, &nonce, &nonce);
540        // "It was a funky funky town!" plus the trailing NUL, as in the reference.
541        let mut message = b"It was a funky funky town!".to_vec();
542        message.push(0);
543
544        let mut encrypted = state.encrypt(&message).expect("encrypt");
545        let body_len = message.len();
546        for i in 0..body_len * 8 {
547            encrypted[i / 8] ^= 1u8 << (i % 8);
548            assert!(
549                state.decrypt(&encrypted).is_none(),
550                "flipped bit {i} must be rejected"
551            );
552            encrypted[i / 8] ^= 1u8 << (i % 8);
553        }
554        assert_eq!(state.decrypt(&encrypted).as_deref(), Some(&message[..]));
555    }
556
557    // REF: TestCrypt.cpp : TestCrypt::ivrecovery.
558    #[test]
559    fn iv_recovery_and_replay_rejection() {
560        let mut enc = CryptState::new(&RAW_KEY, &[0u8; BLOCK_SIZE], &[0u8; BLOCK_SIZE]);
561        // Force the encrypt IV, then align the decryptor to it.
562        let forced = [0x55u8; BLOCK_SIZE];
563        enc.set_encrypt_iv(&forced);
564        let mut dec = CryptState::new(&RAW_KEY, &enc.encrypt_iv(), &enc.encrypt_iv());
565
566        let secret = b"abcdefghi\0";
567
568        let crypted = enc.encrypt(secret).expect("encrypt");
569        assert_eq!(dec.decrypt(&crypted).as_deref(), Some(&secret[..]));
570        // Refuses to reuse the same IV (replay).
571        assert!(dec.decrypt(&crypted).is_none());
572
573        // Recover from lost packets.
574        let mut crypted = crypted;
575        for _ in 0..16 {
576            crypted = enc.encrypt(secret).expect("encrypt");
577        }
578        assert!(dec.decrypt(&crypted).is_some());
579
580        // Wraparound: 15 packets per round, decrypt the last; each round loses 14.
581        for _ in 0..128 {
582            dec.lost = 0;
583            let mut last = Vec::new();
584            for _ in 0..15 {
585                last = enc.encrypt(secret).expect("encrypt");
586            }
587            assert!(dec.decrypt(&last).is_some());
588            assert_eq!(dec.lost, 14);
589        }
590        assert_eq!(enc.encrypt_iv(), dec.decrypt_iv());
591
592        // Wrap too far: 257 packets ahead cannot be recovered.
593        let mut far = Vec::new();
594        for _ in 0..257 {
595            far = enc.encrypt(secret).expect("encrypt");
596        }
597        assert!(dec.decrypt(&far).is_none());
598
599        // Resync and continue.
600        dec.set_decrypt_iv(&enc.encrypt_iv());
601        let next = enc.encrypt(secret).expect("encrypt");
602        assert!(dec.decrypt(&next).is_some());
603    }
604
605    // REF: TestCrypt.cpp : TestCrypt::reverserecovery (out-of-order and replay).
606    #[test]
607    fn reverse_recovery_within_window_then_replay_rejected() {
608        let forced = [0x55u8; BLOCK_SIZE];
609        let mut enc = CryptState::new(&RAW_KEY, &forced, &forced);
610        enc.set_encrypt_iv(&forced);
611        let mut dec = CryptState::new(&RAW_KEY, &enc.encrypt_iv(), &enc.encrypt_iv());
612
613        let secret = b"abcdefghi\0";
614
615        // Encrypt 128 packets, decrypt the most recent 30 in reverse order.
616        let mut packets = Vec::new();
617        for _ in 0..128 {
618            packets.push(enc.encrypt(secret).expect("encrypt"));
619        }
620        for i in 0..30 {
621            assert!(dec.decrypt(&packets[127 - i]).is_some(), "reverse {i}");
622        }
623        // Beyond the recovery window, older packets are rejected.
624        for i in 30..128 {
625            assert!(dec.decrypt(&packets[127 - i]).is_none(), "too old {i}");
626        }
627        // Replaying the already-accepted ones is rejected too.
628        for i in 0..30 {
629            assert!(dec.decrypt(&packets[127 - i]).is_none(), "replay {i}");
630        }
631    }
632}