Skip to main content

hctr2_rs/
hctr2.rs

1#![allow(deprecated)]
2//! HCTR2 (Hash-CTR-Hash) length-preserving wide-block tweakable cipher.
3//!
4//! HCTR2 provides full-block diffusion: any change to plaintext affects the entire ciphertext.
5//! It requires no nonce or authentication tag, making it suitable for constrained environments.
6//!
7//! Construction uses:
8//! - Single encryption key
9//! - Polyval universal hash function
10//! - XCTR mode for wide-block encryption (counter-based)
11//!
12//! Security properties:
13//! - Ciphertext length equals plaintext length (no expansion)
14//! - Requires unique (key, tweak) pairs for security
15//! - No authentication - consider AEAD if integrity protection is needed
16//! - Minimum message length: 16 bytes (one AES block)
17
18#[allow(deprecated)]
19use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
20use aes::{Aes128, Aes256};
21use polyval::{Polyval, universal_hash::UniversalHash};
22
23use crate::common::{BLOCK_LENGTH, Direction, Error, absorb, xctr, xor_blocks, xor_blocks_3};
24
25/// Trait for AES ciphers used in HCTR2.
26pub trait AesCipher: BlockCipherEncrypt + KeyInit + Clone {
27    type Dec: BlockCipherDecrypt + KeyInit;
28    const KEY_LEN: usize;
29
30    fn new_dec(key: &[u8]) -> Self::Dec;
31}
32
33impl AesCipher for Aes128 {
34    type Dec = aes::Aes128Dec;
35    const KEY_LEN: usize = 16;
36
37    fn new_dec(key: &[u8]) -> Self::Dec {
38        aes::Aes128Dec::new(Array::from_slice(key))
39    }
40}
41
42impl AesCipher for Aes256 {
43    type Dec = aes::Aes256Dec;
44    const KEY_LEN: usize = 32;
45
46    fn new_dec(key: &[u8]) -> Self::Dec {
47        aes::Aes256Dec::new(Array::from_slice(key))
48    }
49}
50
51/// Generic HCTR2 cipher parameterized by AES key size.
52pub struct Hctr2<Aes: AesCipher> {
53    ks_enc: Aes,
54    ks_dec: Aes::Dec,
55    h: [u8; BLOCK_LENGTH],
56    l: [u8; BLOCK_LENGTH],
57}
58
59/// HCTR2 with AES-128 encryption.
60#[allow(non_camel_case_types)]
61pub type Hctr2_128 = Hctr2<Aes128>;
62
63/// HCTR2 with AES-256 encryption.
64#[allow(non_camel_case_types)]
65pub type Hctr2_256 = Hctr2<Aes256>;
66
67// Keep the old error type as an alias for backwards compatibility
68#[allow(non_camel_case_types)]
69#[deprecated(note = "Use common::Error instead")]
70pub type Hctr2Error = Error;
71
72impl<Aes: AesCipher> Hctr2<Aes> {
73    /// Encryption key length in bytes.
74    pub const KEY_LENGTH: usize = Aes::KEY_LEN;
75
76    /// AES block length in bytes (always 16).
77    pub const BLOCK_LENGTH: usize = BLOCK_LENGTH;
78
79    /// Minimum input length in bytes.
80    pub const MIN_INPUT_LENGTH: usize = BLOCK_LENGTH;
81
82    /// Initialize HCTR2 cipher state from an encryption key.
83    pub fn new(key: &[u8]) -> Self {
84        debug_assert_eq!(key.len(), Aes::KEY_LEN);
85
86        let ks_enc = Aes::new(Array::from_slice(key));
87        let ks_dec = Aes::new_dec(key);
88
89        let mut block0 = Array::clone_from_slice(&[0u8; 16]);
90        let mut block1 = Array::clone_from_slice(&{
91            let mut b = [0u8; 16];
92            b[0] = 1;
93            b
94        });
95
96        ks_enc.encrypt_block(&mut block0);
97        ks_enc.encrypt_block(&mut block1);
98
99        let h: [u8; 16] = block0.as_slice().try_into().unwrap();
100        let l: [u8; 16] = block1.as_slice().try_into().unwrap();
101        Self {
102            ks_enc,
103            ks_dec,
104            h,
105            l,
106        }
107    }
108
109    /// Encrypt plaintext to ciphertext using HCTR2.
110    ///
111    /// # Arguments
112    /// * `plaintext` - Input data to encrypt (minimum 16 bytes)
113    /// * `tweak` - Tweak value for domain separation
114    /// * `ciphertext` - Output buffer (must be same length as plaintext)
115    ///
116    /// # Errors
117    /// Returns `Error::InputTooShort` if plaintext is less than 16 bytes.
118    pub fn encrypt(
119        &self,
120        plaintext: &[u8],
121        tweak: &[u8],
122        ciphertext: &mut [u8],
123    ) -> Result<(), Error> {
124        self.hctr2(plaintext, tweak, ciphertext, Direction::Encrypt)
125    }
126
127    /// Decrypt ciphertext to plaintext using HCTR2.
128    ///
129    /// # Arguments
130    /// * `ciphertext` - Input data to decrypt (minimum 16 bytes)
131    /// * `tweak` - Tweak value used during encryption
132    /// * `plaintext` - Output buffer (must be same length as ciphertext)
133    ///
134    /// # Errors
135    /// Returns `Error::InputTooShort` if ciphertext is less than 16 bytes.
136    pub fn decrypt(
137        &self,
138        ciphertext: &[u8],
139        tweak: &[u8],
140        plaintext: &mut [u8],
141    ) -> Result<(), Error> {
142        self.hctr2(ciphertext, tweak, plaintext, Direction::Decrypt)
143    }
144
145    fn hctr2(
146        &self,
147        src: &[u8],
148        tweak: &[u8],
149        dst: &mut [u8],
150        direction: Direction,
151    ) -> Result<(), Error> {
152        debug_assert_eq!(dst.len(), src.len());
153        if src.len() < BLOCK_LENGTH {
154            return Err(Error::InputTooShort);
155        }
156
157        let m: [u8; BLOCK_LENGTH] = src[..BLOCK_LENGTH].try_into().unwrap();
158        let n = &src[BLOCK_LENGTH..];
159
160        let tweak_len_bits = tweak.len() * 8;
161        let tweak_len_encoded: u128 = if n.len() % BLOCK_LENGTH == 0 {
162            (2 * tweak_len_bits + 2) as u128
163        } else {
164            (2 * tweak_len_bits + 3) as u128
165        };
166
167        let mut poly = Polyval::new(Array::from_slice(&self.h));
168        poly.update(&[Array::from(tweak_len_encoded.to_le_bytes())]);
169        poly.update_padded(tweak);
170        let poly_after_tweak = poly.clone();
171
172        let hh = absorb(&mut poly, n);
173        let mm = xor_blocks(&hh, &m);
174
175        let uu: [u8; BLOCK_LENGTH] = match direction {
176            Direction::Encrypt => {
177                let mut block = Array::clone_from_slice(&mm);
178                self.ks_enc.encrypt_block(&mut block);
179                block.as_slice().try_into().unwrap()
180            }
181            Direction::Decrypt => {
182                let mut block = Array::clone_from_slice(&mm);
183                self.ks_dec.decrypt_block(&mut block);
184                block.as_slice().try_into().unwrap()
185            }
186        };
187
188        let s = xor_blocks_3(&mm, &uu, &self.l);
189        let (u, v) = dst.split_at_mut(BLOCK_LENGTH);
190        xctr(&self.ks_enc, v, n, &s);
191
192        let mut poly = poly_after_tweak;
193        let hh2 = absorb(&mut poly, v);
194        u.copy_from_slice(&xor_blocks(&uu, &hh2));
195
196        Ok(())
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_hctr2_128_roundtrip() {
206        let key = [0u8; 16];
207        let cipher = Hctr2_128::new(&key);
208
209        let plaintext = b"Hello, HCTR2 World!";
210        let mut ciphertext = vec![0u8; plaintext.len()];
211        let mut decrypted = vec![0u8; plaintext.len()];
212
213        let tweak = b"test tweak";
214
215        cipher.encrypt(plaintext, tweak, &mut ciphertext).unwrap();
216        cipher.decrypt(&ciphertext, tweak, &mut decrypted).unwrap();
217
218        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
219    }
220
221    #[test]
222    fn test_hctr2_128_roundtrip_nonzero_key() {
223        let key = [
224            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
225            0x0f, 0x10,
226        ];
227        let cipher = Hctr2_128::new(&key);
228
229        let plaintext = b"Hello, HCTR2 World!";
230        let mut ciphertext = vec![0u8; plaintext.len()];
231        let mut decrypted = vec![0u8; plaintext.len()];
232
233        let tweak = b"test tweak";
234
235        cipher.encrypt(plaintext, tweak, &mut ciphertext).unwrap();
236        assert_ne!(plaintext.as_slice(), ciphertext.as_slice());
237
238        cipher.decrypt(&ciphertext, tweak, &mut decrypted).unwrap();
239
240        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
241    }
242
243    #[test]
244    fn test_hctr2_128_minimum_length() {
245        let key = [0u8; 16];
246        let cipher = Hctr2_128::new(&key);
247
248        let plaintext = [0x42u8; 16];
249        let mut ciphertext = [0u8; 16];
250        let mut decrypted = [0u8; 16];
251
252        cipher.encrypt(&plaintext, b"", &mut ciphertext).unwrap();
253        cipher.decrypt(&ciphertext, b"", &mut decrypted).unwrap();
254
255        assert_eq!(plaintext, decrypted);
256    }
257
258    #[test]
259    fn test_hctr2_128_input_too_short() {
260        let key = [0u8; 16];
261        let cipher = Hctr2_128::new(&key);
262
263        let plaintext = [0x42u8; 15]; // Too short
264        let mut ciphertext = [0u8; 15];
265
266        assert_eq!(
267            cipher.encrypt(&plaintext, b"", &mut ciphertext),
268            Err(Error::InputTooShort)
269        );
270    }
271
272    #[test]
273    fn test_hctr2_128_different_tweaks() {
274        let key = [0u8; 16];
275        let cipher = Hctr2_128::new(&key);
276
277        let plaintext = [0x42u8; 32];
278        let mut ciphertext1 = [0u8; 32];
279        let mut ciphertext2 = [0u8; 32];
280
281        cipher
282            .encrypt(&plaintext, b"tweak1", &mut ciphertext1)
283            .unwrap();
284        cipher
285            .encrypt(&plaintext, b"tweak2", &mut ciphertext2)
286            .unwrap();
287        assert_ne!(ciphertext1, ciphertext2);
288    }
289
290    #[test]
291    fn test_hctr2_128_large_message() {
292        let key = [0u8; 16];
293        let cipher = Hctr2_128::new(&key);
294
295        let plaintext = [0xABu8; 1024];
296        let mut ciphertext = [0u8; 1024];
297        let mut decrypted = [0u8; 1024];
298
299        cipher
300            .encrypt(&plaintext, b"large tweak", &mut ciphertext)
301            .unwrap();
302        cipher
303            .decrypt(&ciphertext, b"large tweak", &mut decrypted)
304            .unwrap();
305
306        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
307    }
308
309    #[test]
310    fn test_hctr2_256_roundtrip() {
311        let key = [0u8; 32];
312        let cipher = Hctr2_256::new(&key);
313
314        let plaintext = b"Hello, HCTR2-256 World!";
315        let mut ciphertext = vec![0u8; plaintext.len()];
316        let mut decrypted = vec![0u8; plaintext.len()];
317
318        let tweak = b"test tweak 256";
319
320        cipher.encrypt(plaintext, tweak, &mut ciphertext).unwrap();
321        cipher.decrypt(&ciphertext, tweak, &mut decrypted).unwrap();
322
323        assert_eq!(plaintext.as_slice(), decrypted.as_slice());
324    }
325}