Skip to main content

oxicrypto_cipher/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Raw single-block / stream cipher primitives for the OxiCrypto stack.
4//!
5//! These are deliberately *low-level* building blocks, distinct from the
6//! authenticated [`oxicrypto-aead`](https://docs.rs/oxicrypto-aead) ciphers.
7//! They exist for QUIC header protection (RFC 9001 §5.4), which masks packet
8//! headers with a 5-byte sample of either an AES-ECB single-block encryption
9//! (§5.4.3) or a ChaCha20 keystream block (§5.4.4).
10//!
11//! | Primitive | Function | Notes |
12//! |-----------|----------|-------|
13//! | AES-128 single block | [`aes128_encrypt_block`] | one 16-byte ECB block |
14//! | AES-256 single block | [`aes256_encrypt_block`] | one 16-byte ECB block |
15//! | ChaCha20 keystream block | [`chacha20_keystream_block`] | RFC 8439 / RFC 9001 §5.4.4 |
16//!
17//! All wrappers are `#![forbid(unsafe_code)]`; the underlying `aes` / `chacha20`
18//! crates provide safe constructors (`KeyInit::new`, `KeyIvInit::new`) and
19//! operations (`BlockEncrypt::encrypt_block`, `StreamCipher::apply_keystream`).
20
21use aes::cipher::{BlockCipherEncrypt, KeyInit};
22use aes::{Aes128, Aes256};
23use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
24use chacha20::ChaCha20;
25use oxicrypto_core::CryptoError;
26
27/// AES block size in bytes.
28pub const AES_BLOCK_LEN: usize = 16;
29
30/// AES-128 key length in bytes.
31pub const AES128_KEY_LEN: usize = 16;
32
33/// AES-256 key length in bytes.
34pub const AES256_KEY_LEN: usize = 32;
35
36/// Encrypt a single 16-byte block with AES-128 in raw ECB mode.
37///
38/// This is the QUIC header-protection mask primitive for the AES-128 suite
39/// (RFC 9001 §5.4.3): `mask = AES-ECB(hp_key, sample)`.
40///
41/// `key` must be exactly 16 bytes; `block` must be exactly 16 bytes. The
42/// ciphertext block is written into `out` (16 bytes).
43///
44/// # Errors
45/// Returns [`CryptoError::InvalidKey`] if `key` is not 16 bytes,
46/// [`CryptoError::BadInput`] if `block` is not 16 bytes, and
47/// [`CryptoError::BufferTooSmall`] if `out` is shorter than 16 bytes.
48pub fn aes128_encrypt_block(key: &[u8], block: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
49    if key.len() != AES128_KEY_LEN {
50        return Err(CryptoError::InvalidKey);
51    }
52    if block.len() != AES_BLOCK_LEN {
53        return Err(CryptoError::BadInput);
54    }
55    if out.len() < AES_BLOCK_LEN {
56        return Err(CryptoError::BufferTooSmall);
57    }
58    let cipher = Aes128::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
59    let mut buf: aes::Block = aes::Block::try_from(block).map_err(|_| CryptoError::BadInput)?;
60    cipher.encrypt_block(&mut buf);
61    out[..AES_BLOCK_LEN].copy_from_slice(&buf);
62    Ok(())
63}
64
65/// Encrypt a single 16-byte block with AES-256 in raw ECB mode.
66///
67/// QUIC header-protection mask primitive for the AES-256 suite
68/// (RFC 9001 §5.4.3). `key` must be exactly 32 bytes; `block` and `out` are
69/// 16 bytes as for [`aes128_encrypt_block`].
70///
71/// # Errors
72/// As [`aes128_encrypt_block`], but `key` must be 32 bytes.
73pub fn aes256_encrypt_block(key: &[u8], block: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
74    if key.len() != AES256_KEY_LEN {
75        return Err(CryptoError::InvalidKey);
76    }
77    if block.len() != AES_BLOCK_LEN {
78        return Err(CryptoError::BadInput);
79    }
80    if out.len() < AES_BLOCK_LEN {
81        return Err(CryptoError::BufferTooSmall);
82    }
83    let cipher = Aes256::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
84    let mut buf: aes::Block = aes::Block::try_from(block).map_err(|_| CryptoError::BadInput)?;
85    cipher.encrypt_block(&mut buf);
86    out[..AES_BLOCK_LEN].copy_from_slice(&buf);
87    Ok(())
88}
89
90/// ChaCha20 key length in bytes.
91pub const CHACHA20_KEY_LEN: usize = 32;
92
93/// ChaCha20 nonce length in bytes (IETF / RFC 8439 variant).
94pub const CHACHA20_NONCE_LEN: usize = 12;
95
96/// Produce ChaCha20 keystream bytes for a given 32-byte key, 32-bit block
97/// counter, and 12-byte nonce (RFC 8439 / RFC 9001 §5.4.4).
98///
99/// The keystream is generated starting at block `counter` and XORed against an
100/// all-zero buffer, so `out` is filled with raw keystream. For QUIC header
101/// protection the caller passes `counter` taken from the first 4 bytes of the
102/// header-protection sample (little-endian), the nonce from the remaining 12
103/// sample bytes, and a 5-byte `out` buffer to receive the mask.
104///
105/// `out` may be any length up to one ChaCha20 keystream block beyond the
106/// counter that does not overflow the 32-bit counter; for QUIC it is 5 bytes.
107///
108/// # Errors
109/// Returns [`CryptoError::InvalidKey`] if `key` is not 32 bytes,
110/// [`CryptoError::InvalidNonce`] if `nonce` is not 12 bytes, and
111/// [`CryptoError::BadInput`] if `out` is empty.
112pub fn chacha20_keystream_block(
113    key: &[u8],
114    counter: u32,
115    nonce: &[u8],
116    out: &mut [u8],
117) -> Result<(), CryptoError> {
118    if key.len() != CHACHA20_KEY_LEN {
119        return Err(CryptoError::InvalidKey);
120    }
121    if nonce.len() != CHACHA20_NONCE_LEN {
122        return Err(CryptoError::InvalidNonce);
123    }
124    if out.is_empty() {
125        return Err(CryptoError::BadInput);
126    }
127
128    let key_arr: chacha20::Key =
129        chacha20::Key::try_from(key).map_err(|_| CryptoError::InvalidKey)?;
130    let nonce_arr: chacha20::cipher::Iv<ChaCha20> =
131        chacha20::cipher::Iv::<ChaCha20>::try_from(nonce).map_err(|_| CryptoError::InvalidNonce)?;
132    let mut cipher = ChaCha20::new(&key_arr, &nonce_arr);
133    // Seek to the requested 32-bit block counter (counter * 64 bytes).
134    let byte_offset = u64::from(counter)
135        .checked_mul(64)
136        .ok_or(CryptoError::BadInput)?;
137    cipher.seek(byte_offset);
138    // Zero the output then XOR keystream over it -> raw keystream.
139    for b in out.iter_mut() {
140        *b = 0;
141    }
142    cipher.apply_keystream(out);
143    Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn hex_decode(s: &str) -> Vec<u8> {
151        (0..s.len())
152            .step_by(2)
153            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex"))
154            .collect()
155    }
156
157    // FIPS-197 AES-128 known-answer: key = 000102...0f, pt = 00112233...ff,
158    // ct = 69c4e0d86a7b0430d8cdb78070b4c55a.
159    #[test]
160    fn aes128_fips197_appendix_b() {
161        let key = hex_decode("000102030405060708090a0b0c0d0e0f");
162        let pt = hex_decode("00112233445566778899aabbccddeeff");
163        let mut out = [0u8; 16];
164        aes128_encrypt_block(&key, &pt, &mut out).expect("aes128");
165        assert_eq!(out.to_vec(), hex_decode("69c4e0d86a7b0430d8cdb78070b4c55a"));
166    }
167
168    // FIPS-197 AES-256 known-answer: key = 0001...1f, pt = 00112233...ff,
169    // ct = 8ea2b7ca516745bfeafc49904b496089.
170    #[test]
171    fn aes256_fips197_appendix_c() {
172        let key = hex_decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
173        let pt = hex_decode("00112233445566778899aabbccddeeff");
174        let mut out = [0u8; 16];
175        aes256_encrypt_block(&key, &pt, &mut out).expect("aes256");
176        assert_eq!(out.to_vec(), hex_decode("8ea2b7ca516745bfeafc49904b496089"));
177    }
178
179    // RFC 9001 §A.5: ChaCha20 header protection sample.
180    // hp key = 25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4
181    // sample = 5e5cd55c41f69080575d7999c25a5bfb
182    //   counter = 0x5e5cd55c (LE of first 4 bytes) ... actually sample[0..4]
183    //             interpreted little-endian.
184    //   nonce   = sample[4..16]
185    // mask    = aefefe7d03  (first 5 bytes of keystream)
186    #[test]
187    fn rfc9001_a5_chacha20_header_mask() {
188        let hp = hex_decode("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
189        let sample = hex_decode("5e5cd55c41f69080575d7999c25a5bfb");
190        // counter from first 4 bytes, little-endian
191        let counter = u32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]);
192        let nonce = &sample[4..16];
193        let mut mask = [0u8; 5];
194        chacha20_keystream_block(&hp, counter, nonce, &mut mask).expect("ks");
195        assert_eq!(
196            mask.to_vec(),
197            hex_decode("aefefe7d03"),
198            "RFC 9001 A.5 mask mismatch"
199        );
200    }
201
202    #[test]
203    fn aes_invalid_lengths() {
204        let mut out = [0u8; 16];
205        assert_eq!(
206            aes128_encrypt_block(&[0u8; 15], &[0u8; 16], &mut out),
207            Err(CryptoError::InvalidKey)
208        );
209        assert_eq!(
210            aes128_encrypt_block(&[0u8; 16], &[0u8; 15], &mut out),
211            Err(CryptoError::BadInput)
212        );
213        assert_eq!(
214            aes256_encrypt_block(&[0u8; 32], &[0u8; 16], &mut [0u8; 8]),
215            Err(CryptoError::BufferTooSmall)
216        );
217    }
218
219    #[test]
220    fn chacha20_invalid_lengths() {
221        let mut out = [0u8; 5];
222        assert_eq!(
223            chacha20_keystream_block(&[0u8; 31], 0, &[0u8; 12], &mut out),
224            Err(CryptoError::InvalidKey)
225        );
226        assert_eq!(
227            chacha20_keystream_block(&[0u8; 32], 0, &[0u8; 11], &mut out),
228            Err(CryptoError::InvalidNonce)
229        );
230        assert_eq!(
231            chacha20_keystream_block(&[0u8; 32], 0, &[0u8; 12], &mut []),
232            Err(CryptoError::BadInput)
233        );
234    }
235
236    // RFC 8439 §2.4.2 keystream sanity (block counter 1, the documented test
237    // vector starts at counter=1): verify determinism + non-zero output.
238    #[test]
239    fn chacha20_keystream_deterministic() {
240        let key = [0x01u8; 32];
241        let nonce = [0x02u8; 12];
242        let mut a = [0u8; 16];
243        let mut b = [0u8; 16];
244        chacha20_keystream_block(&key, 1, &nonce, &mut a).expect("a");
245        chacha20_keystream_block(&key, 1, &nonce, &mut b).expect("b");
246        assert_eq!(a, b);
247        assert_ne!(a, [0u8; 16]);
248    }
249}