Skip to main content

hctr2_rs/
hctr2pp.rs

1#![allow(deprecated)]
2//! HCTR2++ (HCTR++) beyond-birthday-bound secure variant of HCTR2.
3//!
4//! HCTR2++ is the construction from "HCTR++: A Beyond Birthday Bound Secure HCTR2
5//! Variant" by Ozturk, Kocak and Yayla. It keeps the Hash-Encrypt-Hash structure of
6//! HCTR2 but replaces every static block cipher call with Mennink's R3 fresh re-keying
7//! scheme and widens the universal hash function from n bits to 2n bits. The result is
8//! O(2^n) SPRP security with unique tweaks, degrading gracefully with tweak reuse,
9//! while still being built from a plain block cipher (AES).
10//!
11//! Construction (Figure 4 of the paper):
12//! - Subkeys h1, h2 (2n-bit hash keys) and K (2n-bit re-keying key) are derived from
13//!   the master key as E_K(bin(1))||E_K(bin(2)), E_K(bin(3))||E_K(bin(4)) and
14//!   E_K(bin(5))||E_K(bin(6))
15//! - The first block is masked with the top half of a 2n-bit hash of the bulk and the
16//!   tweak, then encrypted under an R3-derived ephemeral key; the bottom half of the
17//!   hash seeds the re-keying nonce
18//! - The internal state r||J is the 2n-bit hash of CC and the tweak under h1 XOR h2
19//! - CTR++ generates the keystream with a fresh R3-derived key per block
20//! - The first output block goes through an inverse block cipher call so that the
21//!   ciphertext depends on the complete hash digest, and encryption and decryption
22//!   share the same code path with h1 and h2 swapped
23//!
24//! The 2n-bit hash is instantiated as a POLYVAL-style polynomial evaluation over
25//! GF(2^256). Blocks are 32 bytes interpreted as little-endian polynomials, and the
26//! field is defined by the irreducible pentanomial x^256 + x^10 + x^5 + x^2 + 1. The
27//! input encoding mirrors the HCTR2 hash: a length block encoding 2*|T| + 2 (or + 3
28//! when the message is not block-aligned), the zero-padded tweak, then the message
29//! padded with a single 1 bit and zeros.
30//!
31//! Security properties:
32//! - Beyond-birthday-bound SPRP security, approaching O(2^n) with unique tweaks
33//! - Security degrades with the number of queries sharing the same tweak
34//! - Ciphertext length equals plaintext length (no expansion)
35//! - No authentication - consider AEAD if integrity protection is needed
36//! - Minimum message length: 16 bytes (one AES block)
37//!
38//! The re-keying makes this mode noticeably slower than HCTR2: every message block
39//! costs one AES key schedule, one AES call and two GF(2^256) multiplications.
40
41#[allow(deprecated)]
42use aes::Aes128;
43use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
44use core::marker::PhantomData;
45
46use crate::common::{BLOCK_LENGTH, Direction, Error, xor_blocks};
47use crate::hctr2::AesCipher;
48
49/// Hash block length in bytes (2n bits with n = 128).
50pub const HASH_BLOCK_LENGTH: usize = 32;
51
52type Elem = [u64; 4];
53
54/// Carryless 64x64 -> 128 bit multiplication.
55///
56/// Uses the masked-multiplication technique (as in BearSSL's ghash_ctmul64) so the
57/// running time does not depend on the operand values.
58#[inline]
59fn bmul64(x: u64, y: u64) -> u128 {
60    const M0: u128 = 0x1111_1111_1111_1111_1111_1111_1111_1111;
61    const M1: u128 = M0 << 1;
62    const M2: u128 = M0 << 2;
63    const M3: u128 = M0 << 3;
64
65    let x0 = (x as u128) & M0;
66    let x1 = (x as u128) & M1;
67    let x2 = (x as u128) & M2;
68    let x3 = (x as u128) & M3;
69    let y0 = (y as u128) & M0;
70    let y1 = (y as u128) & M1;
71    let y2 = (y as u128) & M2;
72    let y3 = (y as u128) & M3;
73
74    let z0 = (x0 * y0) ^ (x1 * y3) ^ (x2 * y2) ^ (x3 * y1);
75    let z1 = (x0 * y1) ^ (x1 * y0) ^ (x2 * y3) ^ (x3 * y2);
76    let z2 = (x0 * y2) ^ (x1 * y1) ^ (x2 * y0) ^ (x3 * y3);
77    let z3 = (x0 * y3) ^ (x1 * y2) ^ (x2 * y1) ^ (x3 * y0);
78
79    (z0 & M0) | (z1 & M1) | (z2 & M2) | (z3 & M3)
80}
81
82/// Multiplication in GF(2^256) defined by x^256 + x^10 + x^5 + x^2 + 1.
83///
84/// Elements are little-endian: bit i of limb j is the coefficient of x^(64j + i).
85fn gf_mul(a: &Elem, b: &Elem) -> Elem {
86    let mut t = [0u64; 8];
87    for i in 0..4 {
88        for j in 0..4 {
89            let p = bmul64(a[i], b[j]);
90            t[i + j] ^= p as u64;
91            t[i + j + 1] ^= (p >> 64) as u64;
92        }
93    }
94
95    // Fold the upper 256 bits using x^256 = x^10 + x^5 + x^2 + 1.
96    let hi = [t[4], t[5], t[6], t[7]];
97    let mut res = [t[0], t[1], t[2], t[3]];
98    let mut overflow: u64 = 0;
99    for k in 0..4 {
100        res[k] ^= hi[k];
101    }
102    for s in [2u32, 5, 10] {
103        let mut prev: u64 = 0;
104        for k in 0..4 {
105            res[k] ^= (hi[k] << s) | (prev >> (64 - s));
106            prev = hi[k];
107        }
108        overflow ^= hi[3] >> (64 - s);
109    }
110    // overflow < 2^10, so the second fold stays within the first limb.
111    res[0] ^= overflow ^ (overflow << 2) ^ (overflow << 5) ^ (overflow << 10);
112    res
113}
114
115#[inline]
116fn elem_from_bytes(bytes: &[u8; HASH_BLOCK_LENGTH]) -> Elem {
117    let mut e = [0u64; 4];
118    for (i, limb) in e.iter_mut().enumerate() {
119        *limb = u64::from_le_bytes(bytes[i * 8..(i + 1) * 8].try_into().unwrap());
120    }
121    e
122}
123
124#[inline]
125fn elem_to_bytes(e: &Elem) -> [u8; HASH_BLOCK_LENGTH] {
126    let mut bytes = [0u8; HASH_BLOCK_LENGTH];
127    for (i, limb) in e.iter().enumerate() {
128        bytes[i * 8..(i + 1) * 8].copy_from_slice(&limb.to_le_bytes());
129    }
130    bytes
131}
132
133#[inline]
134fn elem_xor(a: &Elem, b: &Elem) -> Elem {
135    [a[0] ^ b[0], a[1] ^ b[1], a[2] ^ b[2], a[3] ^ b[3]]
136}
137
138/// 2n-bit POLYVAL-style universal hash over GF(2^256).
139///
140/// Computes sum X_i * h^(l-i+1) by Horner evaluation, with the HCTR2 input encoding:
141/// a length block, the zero-padded tweak, and the message padded with 1 || 0*.
142fn hash2n(key: &Elem, tweak: &[u8], msg: &[u8]) -> [u8; HASH_BLOCK_LENGTH] {
143    let tweak_len_bits = tweak.len() * 8;
144    let len_code: u128 = if msg.len().is_multiple_of(HASH_BLOCK_LENGTH) {
145        (2 * tweak_len_bits + 2) as u128
146    } else {
147        (2 * tweak_len_bits + 3) as u128
148    };
149    let mut len_block = [0u8; HASH_BLOCK_LENGTH];
150    len_block[..16].copy_from_slice(&len_code.to_le_bytes());
151
152    let mut acc = [0u64; 4];
153    let mut update = |block: &[u8; HASH_BLOCK_LENGTH]| {
154        acc = gf_mul(&elem_xor(&acc, &elem_from_bytes(block)), key);
155    };
156
157    update(&len_block);
158
159    let mut chunks = tweak.chunks_exact(HASH_BLOCK_LENGTH);
160    for chunk in chunks.by_ref() {
161        update(chunk.try_into().unwrap());
162    }
163    let rem = chunks.remainder();
164    if !rem.is_empty() {
165        let mut block = [0u8; HASH_BLOCK_LENGTH];
166        block[..rem.len()].copy_from_slice(rem);
167        update(&block);
168    }
169
170    let mut chunks = msg.chunks_exact(HASH_BLOCK_LENGTH);
171    for chunk in chunks.by_ref() {
172        update(chunk.try_into().unwrap());
173    }
174    let rem = chunks.remainder();
175    if !rem.is_empty() {
176        let mut block = [0u8; HASH_BLOCK_LENGTH];
177        block[..rem.len()].copy_from_slice(rem);
178        block[rem.len()] = 1;
179        update(&block);
180    }
181
182    elem_to_bytes(&acc)
183}
184
185/// Generic HCTR2++ cipher parameterized by the master AES key size.
186///
187/// The master cipher is only used to derive the hash and re-keying subkeys. The
188/// re-keyed block cipher calls always use AES-128, since the R3 scheme produces n-bit
189/// ephemeral keys; the effective key strength is therefore 128 bits regardless of the
190/// master key size.
191pub struct Hctr2pp<Aes: AesCipher> {
192    h1: Elem,
193    h2: Elem,
194    h12: Elem,
195    kk: Elem,
196    _aes: PhantomData<Aes>,
197}
198
199/// HCTR2++ with an AES-128 master key.
200#[allow(non_camel_case_types)]
201pub type Hctr2pp_128 = Hctr2pp<Aes128>;
202
203/// HCTR2++ with an AES-256 master key.
204///
205/// AES-256 is used only to derive the hash and re-keying subkeys from the master
206/// key. The R3 re-keyed block cipher calls are always AES-128, because the R3
207/// scheme produces n-bit ephemeral keys, so this variant does NOT provide
208/// 256-bit security: the effective primitive strength is 128 bits, exactly as
209/// with [`Hctr2pp_128`]. Choose it only when key management mandates a 256-bit
210/// master key.
211#[allow(non_camel_case_types)]
212pub type Hctr2pp_256 = Hctr2pp<aes::Aes256>;
213
214impl<Aes: AesCipher> Hctr2pp<Aes> {
215    /// Master key length in bytes.
216    pub const KEY_LENGTH: usize = Aes::KEY_LEN;
217
218    /// AES block length in bytes (always 16).
219    pub const BLOCK_LENGTH: usize = BLOCK_LENGTH;
220
221    /// Minimum input length in bytes.
222    pub const MIN_INPUT_LENGTH: usize = BLOCK_LENGTH;
223
224    /// Initialize HCTR2++ cipher state from a master key.
225    pub fn new(key: &[u8]) -> Self {
226        debug_assert_eq!(key.len(), Aes::KEY_LEN);
227
228        let ks = Aes::new(Array::from_slice(key));
229        let mut subkeys = [[0u8; BLOCK_LENGTH]; 6];
230        for (i, subkey) in subkeys.iter_mut().enumerate() {
231            let mut block = [0u8; BLOCK_LENGTH];
232            block[0] = (i + 1) as u8;
233            let mut arr = Array::clone_from_slice(&block);
234            ks.encrypt_block(&mut arr);
235            *subkey = arr.as_slice().try_into().unwrap();
236        }
237
238        let pair = |a: &[u8; 16], b: &[u8; 16]| {
239            let mut bytes = [0u8; HASH_BLOCK_LENGTH];
240            bytes[..16].copy_from_slice(a);
241            bytes[16..].copy_from_slice(b);
242            elem_from_bytes(&bytes)
243        };
244        let h1 = pair(&subkeys[0], &subkeys[1]);
245        let h2 = pair(&subkeys[2], &subkeys[3]);
246        let kk = pair(&subkeys[4], &subkeys[5]);
247
248        Self {
249            h1,
250            h2,
251            h12: elem_xor(&h1, &h2),
252            kk,
253            _aes: PhantomData,
254        }
255    }
256
257    /// Encrypt plaintext to ciphertext using HCTR2++.
258    ///
259    /// # Arguments
260    /// * `plaintext` - Input data to encrypt (minimum 16 bytes)
261    /// * `tweak` - Tweak value for domain separation
262    /// * `ciphertext` - Output buffer (must be same length as plaintext)
263    ///
264    /// # Errors
265    /// Returns `Error::InputTooShort` if plaintext is less than 16 bytes, and
266    /// `Error::OutputLengthMismatch` if the buffer lengths differ.
267    pub fn encrypt(
268        &self,
269        plaintext: &[u8],
270        tweak: &[u8],
271        ciphertext: &mut [u8],
272    ) -> Result<(), Error> {
273        self.hctr2pp(plaintext, tweak, ciphertext, Direction::Encrypt)
274    }
275
276    /// Decrypt ciphertext to plaintext using HCTR2++.
277    ///
278    /// # Arguments
279    /// * `ciphertext` - Input data to decrypt (minimum 16 bytes)
280    /// * `tweak` - Tweak value used during encryption
281    /// * `plaintext` - Output buffer (must be same length as ciphertext)
282    ///
283    /// # Errors
284    /// Returns `Error::InputTooShort` if ciphertext is less than 16 bytes, and
285    /// `Error::OutputLengthMismatch` if the buffer lengths differ.
286    pub fn decrypt(
287        &self,
288        ciphertext: &[u8],
289        tweak: &[u8],
290        plaintext: &mut [u8],
291    ) -> Result<(), Error> {
292        self.hctr2pp(ciphertext, tweak, plaintext, Direction::Decrypt)
293    }
294
295    /// R3 re-keying: derive an ephemeral key and mask from a nonce.
296    ///
297    /// The paper leaves H_K(r) underspecified; this implementation hashes r as a
298    /// message with an empty tweak, using the same hash as the rest of the mode.
299    fn r3(&self, r: &[u8; BLOCK_LENGTH]) -> ([u8; BLOCK_LENGTH], [u8; BLOCK_LENGTH]) {
300        let digest = hash2n(&self.kk, b"", r);
301        (
302            digest[..BLOCK_LENGTH].try_into().unwrap(),
303            digest[BLOCK_LENGTH..].try_into().unwrap(),
304        )
305    }
306
307    fn hctr2pp(
308        &self,
309        src: &[u8],
310        tweak: &[u8],
311        dst: &mut [u8],
312        direction: Direction,
313    ) -> Result<(), Error> {
314        if dst.len() != src.len() {
315            return Err(Error::OutputLengthMismatch);
316        }
317        if src.len() < BLOCK_LENGTH {
318            return Err(Error::InputTooShort);
319        }
320
321        // Encryption hashes the input bulk with h1 and the output bulk with h2;
322        // decryption swaps the two, which makes the code path self-inverse.
323        let (hk_in, hk_out) = match direction {
324            Direction::Encrypt => (&self.h1, &self.h2),
325            Direction::Decrypt => (&self.h2, &self.h1),
326        };
327
328        let first: [u8; BLOCK_LENGTH] = src[..BLOCK_LENGTH].try_into().unwrap();
329        let bulk = &src[BLOCK_LENGTH..];
330
331        let x1 = hash2n(hk_in, tweak, bulk);
332        let pp1 = xor_blocks(&first, &x1[..BLOCK_LENGTH].try_into().unwrap());
333        let re: [u8; BLOCK_LENGTH] = x1[BLOCK_LENGTH..].try_into().unwrap();
334
335        let (ue, ve) = self.r3(&re);
336        let mut cc_block = Array::from(xor_blocks(&pp1, &ve));
337        Aes128::new(Array::from_slice(&ue)).encrypt_block(&mut cc_block);
338        let cc = xor_blocks(&cc_block.into(), &ve);
339
340        let rj = hash2n(&self.h12, tweak, &cc);
341        let r: [u8; BLOCK_LENGTH] = rj[..BLOCK_LENGTH].try_into().unwrap();
342        let j: [u8; BLOCK_LENGTH] = rj[BLOCK_LENGTH..].try_into().unwrap();
343
344        let (dst_first, dst_bulk) = dst.split_at_mut(BLOCK_LENGTH);
345        self.ctr_pp(&r, &j, bulk, dst_bulk);
346
347        let x2 = hash2n(hk_out, tweak, dst_bulk);
348        let rd: [u8; BLOCK_LENGTH] = x2[BLOCK_LENGTH..].try_into().unwrap();
349
350        let (ud, vd) = self.r3(&rd);
351        let mut pp2_block = Array::from(xor_blocks(&cc, &vd));
352        aes::Aes128Dec::new(Array::from_slice(&ud)).decrypt_block(&mut pp2_block);
353        let pp2 = xor_blocks(&pp2_block.into(), &vd);
354
355        dst_first.copy_from_slice(&xor_blocks(&pp2, &x2[..BLOCK_LENGTH].try_into().unwrap()));
356
357        Ok(())
358    }
359
360    /// CTR++ mode: counter keystream where every block uses a fresh R3-derived key.
361    fn ctr_pp(&self, r: &[u8; BLOCK_LENGTH], j: &[u8; BLOCK_LENGTH], src: &[u8], dst: &mut [u8]) {
362        for (i, (src_chunk, dst_chunk)) in src
363            .chunks(BLOCK_LENGTH)
364            .zip(dst.chunks_mut(BLOCK_LENGTH))
365            .enumerate()
366        {
367            let counter = ((i + 1) as u128).to_le_bytes();
368            let ri = xor_blocks(r, &counter);
369            let ji = xor_blocks(j, &counter);
370
371            let (ui, vi) = self.r3(&ri);
372            let mut block = Array::from(xor_blocks(&ji, &vi));
373            Aes128::new(Array::from_slice(&ui)).encrypt_block(&mut block);
374            let si = xor_blocks(&block.into(), &vi);
375
376            for (k, (d, s)) in dst_chunk.iter_mut().zip(src_chunk.iter()).enumerate() {
377                *d = s ^ si[k];
378            }
379        }
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    fn xorshift(state: &mut u64) -> u64 {
388        *state ^= *state << 13;
389        *state ^= *state >> 7;
390        *state ^= *state << 17;
391        *state
392    }
393
394    fn random_elem(state: &mut u64) -> Elem {
395        [
396            xorshift(state),
397            xorshift(state),
398            xorshift(state),
399            xorshift(state),
400        ]
401    }
402
403    /// Bit-by-bit GF(2^256) multiplication used as a reference for gf_mul.
404    fn slow_mul(a: &Elem, b: &Elem) -> Elem {
405        let mut res = [0u64; 4];
406        let mut cur = *a;
407        for limb in b.iter() {
408            for bit in 0..64 {
409                if (limb >> bit) & 1 == 1 {
410                    res = elem_xor(&res, &cur);
411                }
412                // cur = cur * x mod x^256 + x^10 + x^5 + x^2 + 1
413                let carry = cur[3] >> 63;
414                cur[3] = (cur[3] << 1) | (cur[2] >> 63);
415                cur[2] = (cur[2] << 1) | (cur[1] >> 63);
416                cur[1] = (cur[1] << 1) | (cur[0] >> 63);
417                cur[0] = (cur[0] << 1) ^ (carry * 0x425);
418            }
419        }
420        res
421    }
422
423    #[test]
424    fn test_gf_mul_matches_reference() {
425        let mut state = 0x1234_5678_9abc_def0u64;
426        for _ in 0..50 {
427            let a = random_elem(&mut state);
428            let b = random_elem(&mut state);
429            assert_eq!(gf_mul(&a, &b), slow_mul(&a, &b));
430        }
431    }
432
433    #[test]
434    fn test_gf_mul_identity_and_commutativity() {
435        let one: Elem = [1, 0, 0, 0];
436        let mut state = 0xdead_beef_cafe_f00du64;
437        for _ in 0..20 {
438            let a = random_elem(&mut state);
439            let b = random_elem(&mut state);
440            assert_eq!(gf_mul(&a, &one), a);
441            assert_eq!(gf_mul(&a, &b), gf_mul(&b, &a));
442        }
443    }
444
445    #[test]
446    fn test_hash2n_separates_inputs() {
447        let key: Elem = [2, 0, 0, 0];
448        assert_ne!(hash2n(&key, b"", b"a"), hash2n(&key, b"a", b""));
449        assert_ne!(hash2n(&key, b"t", b"m"), hash2n(&key, b"t", b"m\0"));
450        assert_ne!(hash2n(&key, b"", &[0u8; 32]), hash2n(&key, b"", &[0u8; 64]));
451    }
452
453    #[test]
454    fn test_hctr2pp_128_roundtrip() {
455        let key = [0u8; 16];
456        let cipher = Hctr2pp_128::new(&key);
457
458        let plaintext = b"Hello, HCTR2++ World!";
459        let mut ciphertext = vec![0u8; plaintext.len()];
460        let mut decrypted = vec![0u8; plaintext.len()];
461
462        let tweak = b"test tweak";
463
464        cipher.encrypt(plaintext, tweak, &mut ciphertext).unwrap();
465        assert_ne!(plaintext.as_slice(), ciphertext.as_slice());
466
467        cipher.decrypt(&ciphertext, tweak, &mut decrypted).unwrap();
468        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
469    }
470
471    #[test]
472    fn test_hctr2pp_128_roundtrip_all_lengths() {
473        let key: [u8; 16] = core::array::from_fn(|i| (i * 7 + 1) as u8);
474        let cipher = Hctr2pp_128::new(&key);
475        let tweak = b"length sweep";
476
477        let plaintext: Vec<u8> = (0..100).map(|i| (i * 13 + 5) as u8).collect();
478        for len in 16..=plaintext.len() {
479            let mut ciphertext = vec![0u8; len];
480            let mut decrypted = vec![0u8; len];
481            cipher
482                .encrypt(&plaintext[..len], tweak, &mut ciphertext)
483                .unwrap();
484            cipher.decrypt(&ciphertext, tweak, &mut decrypted).unwrap();
485            assert_eq!(&plaintext[..len], decrypted.as_slice(), "len = {len}");
486        }
487    }
488
489    #[test]
490    fn test_hctr2pp_128_minimum_length() {
491        let key = [0u8; 16];
492        let cipher = Hctr2pp_128::new(&key);
493
494        let plaintext = [0x42u8; 16];
495        let mut ciphertext = [0u8; 16];
496        let mut decrypted = [0u8; 16];
497
498        cipher.encrypt(&plaintext, b"", &mut ciphertext).unwrap();
499        cipher.decrypt(&ciphertext, b"", &mut decrypted).unwrap();
500
501        assert_eq!(plaintext, decrypted);
502    }
503
504    #[test]
505    fn test_hctr2pp_128_input_too_short() {
506        let key = [0u8; 16];
507        let cipher = Hctr2pp_128::new(&key);
508
509        let plaintext = [0x42u8; 15];
510        let mut ciphertext = [0u8; 15];
511
512        assert_eq!(
513            cipher.encrypt(&plaintext, b"", &mut ciphertext),
514            Err(Error::InputTooShort)
515        );
516    }
517
518    #[test]
519    fn test_hctr2pp_128_different_tweaks() {
520        let key = [0u8; 16];
521        let cipher = Hctr2pp_128::new(&key);
522
523        let plaintext = [0x42u8; 32];
524        let mut ciphertext1 = [0u8; 32];
525        let mut ciphertext2 = [0u8; 32];
526
527        cipher
528            .encrypt(&plaintext, b"tweak1", &mut ciphertext1)
529            .unwrap();
530        cipher
531            .encrypt(&plaintext, b"tweak2", &mut ciphertext2)
532            .unwrap();
533        assert_ne!(ciphertext1, ciphertext2);
534    }
535
536    #[test]
537    fn test_hctr2pp_128_deterministic() {
538        let key: [u8; 16] = core::array::from_fn(|i| i as u8);
539        let cipher1 = Hctr2pp_128::new(&key);
540        let cipher2 = Hctr2pp_128::new(&key);
541
542        let plaintext = [0x55u8; 64];
543        let mut ciphertext1 = [0u8; 64];
544        let mut ciphertext2 = [0u8; 64];
545
546        cipher1.encrypt(&plaintext, b"t", &mut ciphertext1).unwrap();
547        cipher2.encrypt(&plaintext, b"t", &mut ciphertext2).unwrap();
548        assert_eq!(ciphertext1, ciphertext2);
549    }
550
551    #[test]
552    fn test_hctr2pp_128_avalanche() {
553        let key = [7u8; 16];
554        let cipher = Hctr2pp_128::new(&key);
555
556        let plaintext = [0u8; 64];
557        let mut modified = plaintext;
558        modified[63] ^= 1;
559
560        let mut ciphertext1 = [0u8; 64];
561        let mut ciphertext2 = [0u8; 64];
562        cipher.encrypt(&plaintext, b"t", &mut ciphertext1).unwrap();
563        cipher.encrypt(&modified, b"t", &mut ciphertext2).unwrap();
564
565        // Flipping the last plaintext bit must change the first ciphertext block.
566        assert_ne!(ciphertext1[..16], ciphertext2[..16]);
567        // ... and the keystream over the bulk as well.
568        assert_ne!(ciphertext1[16..48], ciphertext2[16..48]);
569    }
570
571    #[test]
572    fn test_hctr2pp_128_large_message() {
573        let key = [0u8; 16];
574        let cipher = Hctr2pp_128::new(&key);
575
576        let plaintext = [0xABu8; 1024];
577        let mut ciphertext = [0u8; 1024];
578        let mut decrypted = [0u8; 1024];
579
580        cipher
581            .encrypt(&plaintext, b"large tweak", &mut ciphertext)
582            .unwrap();
583        cipher
584            .decrypt(&ciphertext, b"large tweak", &mut decrypted)
585            .unwrap();
586
587        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
588    }
589
590    #[test]
591    fn test_hctr2pp_256_roundtrip() {
592        let key = [0u8; 32];
593        let cipher = Hctr2pp_256::new(&key);
594
595        let plaintext = b"Hello, HCTR2++-256 World!";
596        let mut ciphertext = vec![0u8; plaintext.len()];
597        let mut decrypted = vec![0u8; plaintext.len()];
598
599        let tweak = b"test tweak 256";
600
601        cipher.encrypt(plaintext, tweak, &mut ciphertext).unwrap();
602        cipher.decrypt(&ciphertext, tweak, &mut decrypted).unwrap();
603
604        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
605    }
606
607    fn unhex(s: &str) -> Vec<u8> {
608        (0..s.len())
609            .step_by(2)
610            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
611            .collect()
612    }
613
614    // Known-answer vectors generated with an independent Python implementation
615    // of the Figure 4 pseudocode (tmp/hctr2pp_ref.py): big-int GF(2^256)
616    // arithmetic and AES from the cryptography library, no shared code.
617
618    #[test]
619    fn test_kat_subkeys() {
620        let key: [u8; 16] = core::array::from_fn(|i| i as u8);
621        let cipher = Hctr2pp_128::new(&key);
622
623        let expect = |hex: &str| elem_from_bytes(&unhex(hex).try_into().unwrap());
624        assert_eq!(
625            cipher.h1,
626            expect("e37cd363dd7c87a09aff0e3e60e09c82fb8ae31ba5db9cad97364d8722d47326")
627        );
628        assert_eq!(
629            cipher.h2,
630            expect("8cb899148f1fa8ff9132d0eb15a936f2f08c8d049312eac76f8fa05078178aa1")
631        );
632        assert_eq!(
633            cipher.kk,
634            expect("789dc76ccb52ce1c3db90ecb357af60eeb3ee851461107fec27297b27ad5e563")
635        );
636    }
637
638    #[test]
639    fn test_kat_hash2n() {
640        let key: [u8; 16] = core::array::from_fn(|i| i as u8);
641        let cipher = Hctr2pp_128::new(&key);
642
643        let msg16: [u8; 16] = core::array::from_fn(|i| i as u8);
644        let msg40: [u8; 40] = core::array::from_fn(|i| i as u8);
645        let cases: [(&[u8], &[u8], &str); 4] = [
646            (
647                b"",
648                b"",
649                "c6f9a6c7baf90e4135ff1d7cc0c03905f715c7374ab7395b2f6d9a0e45a8e74c",
650            ),
651            (
652                b"tweak",
653                b"",
654                "3c4414e8acf1f1bc7f9674c74043855751ef29e9513bdcf7fec97f76776ec5d6",
655            ),
656            (
657                b"",
658                &msg16,
659                "6bcd860a4f8467fee40f2ae865cf74f010994a07f5c9b0248be2ffff87f29fad",
660            ),
661            (
662                b"tweak",
663                &msg40,
664                "89e96782de662ce1592258b6abc040597d3abaea2e6c86ffee481ff7afb9b858",
665            ),
666        ];
667        for (tweak, msg, expected) in cases {
668            assert_eq!(hash2n(&cipher.h1, tweak, msg).to_vec(), unhex(expected));
669        }
670    }
671
672    #[test]
673    fn test_kat_r3() {
674        let key: [u8; 16] = core::array::from_fn(|i| i as u8);
675        let cipher = Hctr2pp_128::new(&key);
676
677        let r: [u8; 16] = core::array::from_fn(|i| i as u8);
678        let (u, v) = cipher.r3(&r);
679        assert_eq!(u.to_vec(), unhex("cf53026c002409032271b44d2cfb76ae"));
680        assert_eq!(v.to_vec(), unhex("cc0108efd8d927ec03a148f06bec9c38"));
681    }
682
683    #[test]
684    fn test_kat_encrypt_128() {
685        let key: [u8; 16] = core::array::from_fn(|i| i as u8);
686        let cipher = Hctr2pp_128::new(&key);
687
688        let cases: [(&[u8], &str, &str); 3] = [
689            (
690                b"",
691                "000102030405060708090a0b0c0d0e0f",
692                "02c3c1ea3033a092e21a16acaf88121b",
693            ),
694            (
695                b"tweak",
696                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
697                "46feab9309b3e5826370ad00561963c698a293833efa5ce25c23786df2f4abad",
698            ),
699            (
700                &unhex(
701                    "0104070a0d101316191c1f2225282b2e3134373a3d404346494c4f5255585b5e6164676a\
702                     6d707376797c7f8285888b8e9194979a9da0a3a6a9acafb2b5b8bbbec1c4c7cacdd0",
703                ),
704                "050c131a21282f363d444b525960676e757c838a91989fa6adb4bbc2c9d0d7dee5ecf3fa\
705                 01080f161d242b323940474e55",
706                "8ce2a701bc7547a38212d07087d365b4bf6b4bfe530436ec11f9087b813457a013fcfc18\
707                 9b0ca9e172c2a3c148ab621beb",
708            ),
709        ];
710        for (tweak, pt_hex, ct_hex) in cases {
711            let pt = unhex(pt_hex);
712            let expected_ct = unhex(ct_hex);
713            let mut ct = vec![0u8; pt.len()];
714            cipher.encrypt(&pt, tweak, &mut ct).unwrap();
715            assert_eq!(ct, expected_ct);
716
717            let mut decrypted = vec![0u8; ct.len()];
718            cipher.decrypt(&ct, tweak, &mut decrypted).unwrap();
719            assert_eq!(decrypted, pt);
720        }
721    }
722
723    #[test]
724    fn test_kat_encrypt_256() {
725        let key: [u8; 32] = core::array::from_fn(|i| i as u8);
726        let cipher = Hctr2pp_256::new(&key);
727
728        let pt = unhex("03080d12171c21262b30353a3f44494e53585d62676c71767b80858a8f94999ea3");
729        let expected_ct =
730            unhex("d885008594f696577cd7bf4478d27965d8741fd5eda96612e8ef397df08a4d0fec");
731        let mut ct = vec![0u8; pt.len()];
732        cipher.encrypt(&pt, b"tweak 256", &mut ct).unwrap();
733        assert_eq!(ct, expected_ct);
734
735        let mut decrypted = vec![0u8; ct.len()];
736        cipher.decrypt(&ct, b"tweak 256", &mut decrypted).unwrap();
737        assert_eq!(decrypted, pt);
738    }
739
740    #[test]
741    fn test_hctr2pp_128_output_length_mismatch() {
742        let key = [0u8; 16];
743        let cipher = Hctr2pp_128::new(&key);
744
745        let plaintext = [0x42u8; 32];
746        let mut too_short = [0u8; 16];
747        let mut too_long = [0u8; 48];
748
749        assert_eq!(
750            cipher.encrypt(&plaintext, b"", &mut too_short),
751            Err(Error::OutputLengthMismatch)
752        );
753        assert_eq!(
754            cipher.decrypt(&plaintext, b"", &mut too_long),
755            Err(Error::OutputLengthMismatch)
756        );
757    }
758
759    #[test]
760    fn test_hctr2pp_128_long_tweak() {
761        let key = [3u8; 16];
762        let cipher = Hctr2pp_128::new(&key);
763
764        let plaintext = [0x11u8; 40];
765        let tweak = [0x77u8; 100];
766        let mut ciphertext = [0u8; 40];
767        let mut decrypted = [0u8; 40];
768
769        cipher.encrypt(&plaintext, &tweak, &mut ciphertext).unwrap();
770        cipher.decrypt(&ciphertext, &tweak, &mut decrypted).unwrap();
771        assert_eq!(plaintext, decrypted);
772    }
773}