Skip to main content

nectar_primitives/chunk/encryption/
cipher.rs

1//! Keccak-256 counter-mode stream cipher.
2//!
3//! Binary-compatible with the Swarm reference implementation.
4
5use alloy_primitives::Keccak256;
6
7use super::error::EncryptionError;
8use super::key::EncryptionKey;
9
10/// Precompute the hasher state with the key absorbed.
11///
12/// Each segment key derivation hashes `key || LE32(counter)`. Since the key
13/// is constant across all segments, we absorb it once and clone per segment.
14#[inline]
15fn key_state(key: &EncryptionKey) -> Keccak256 {
16    let mut h = Keccak256::new();
17    h.update(key.as_bytes());
18    h
19}
20
21/// Derive keystream block: `Keccak256(Keccak256(key || LE32(counter)))`.
22#[inline]
23fn derive_segment_key(key_state: &Keccak256, counter: u32) -> [u8; 32] {
24    let mut h1 = key_state.clone();
25    h1.update(counter.to_le_bytes());
26    let round1 = h1.finalize();
27
28    let mut h2 = Keccak256::new();
29    h2.update(round1.as_slice());
30    h2.finalize().into()
31}
32
33/// XOR `data` with the Keccak-256 CTR keystream in place.
34#[inline]
35fn apply_keystream(key: &EncryptionKey, init_ctr: u32, data: &mut [u8]) {
36    let ks = key_state(key);
37    for (i, chunk) in data.chunks_mut(EncryptionKey::SIZE).enumerate() {
38        let seg = derive_segment_key(&ks, init_ctr.wrapping_add(i as u32));
39        for (j, byte) in chunk.iter_mut().enumerate() {
40            *byte ^= seg[j];
41        }
42    }
43}
44
45/// XOR `input` with the Keccak-256 CTR keystream, writing to `output`.
46///
47/// The keystream is generated by `derive_segment_key` for each 32-byte block,
48/// starting at `init_ctr` and incrementing by 1 per block.
49///
50/// `output` must be at least as long as `input`. Bytes in `output` beyond
51/// `input.len()` are left untouched.
52#[inline]
53pub fn transcrypt(
54    key: &EncryptionKey,
55    init_ctr: u32,
56    input: &[u8],
57    output: &mut [u8],
58) -> Result<(), EncryptionError> {
59    if output.len() < input.len() {
60        return Err(EncryptionError::OutputBufferTooSmall {
61            len: output.len(),
62            required: input.len(),
63        });
64    }
65    output[..input.len()].copy_from_slice(input);
66    apply_keystream(key, init_ctr, &mut output[..input.len()]);
67    Ok(())
68}
69
70/// XOR `data` with the Keccak-256 CTR keystream in place.
71///
72/// Equivalent to `transcrypt` but operates on a single mutable buffer,
73/// avoiding a separate output allocation.
74#[inline]
75pub fn transcrypt_in_place(key: &EncryptionKey, init_ctr: u32, data: &mut [u8]) {
76    apply_keystream(key, init_ctr, data);
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    /// Test vector: key=8abf1502..., 4096 zero bytes, init_ctr=0.
84    #[test]
85    fn go_test_vector() {
86        let key_hex = "8abf1502f557f15026716030fb6384792583daf39608a3cd02ff2f47e9bc6e49";
87        let key_bytes: [u8; 32] = hex::decode(key_hex).unwrap().try_into().unwrap();
88        let key = EncryptionKey::from(key_bytes);
89
90        let input = [0u8; 4096];
91        let mut output = [0u8; 4096];
92        transcrypt(&key, 0, &input, &mut output).unwrap();
93
94        let expected_hex = include_str!("testdata/go_vector_4096.hex");
95        let expected = hex::decode(expected_hex.trim()).unwrap();
96        assert_eq!(output.as_slice(), expected.as_slice());
97    }
98
99    #[test]
100    fn go_test_vector_in_place() {
101        let key_hex = "8abf1502f557f15026716030fb6384792583daf39608a3cd02ff2f47e9bc6e49";
102        let key_bytes: [u8; 32] = hex::decode(key_hex).unwrap().try_into().unwrap();
103        let key = EncryptionKey::from(key_bytes);
104
105        let mut data = [0u8; 4096];
106        transcrypt_in_place(&key, 0, &mut data);
107
108        let expected_hex = include_str!("testdata/go_vector_4096.hex");
109        let expected = hex::decode(expected_hex.trim()).unwrap();
110        assert_eq!(data.as_slice(), expected.as_slice());
111    }
112
113    #[test]
114    fn symmetry() {
115        let key = EncryptionKey::from([0x42; 32]);
116        let plaintext = b"hello world, this is a test!!!!!"; // 32 bytes
117        let mut ciphertext = [0u8; 32];
118        let mut recovered = [0u8; 32];
119
120        transcrypt(&key, 0, plaintext, &mut ciphertext).unwrap();
121        assert_ne!(&ciphertext[..], plaintext);
122
123        transcrypt(&key, 0, &ciphertext, &mut recovered).unwrap();
124        assert_eq!(&recovered[..], plaintext);
125    }
126
127    #[test]
128    fn in_place_symmetry() {
129        let key = EncryptionKey::from([0x42; 32]);
130        let original = *b"hello world, this is a test!!!!!";
131        let mut data = original;
132
133        transcrypt_in_place(&key, 0, &mut data);
134        assert_ne!(data, original);
135
136        transcrypt_in_place(&key, 0, &mut data);
137        assert_eq!(data, original);
138    }
139
140    #[test]
141    fn in_place_matches_transcrypt() {
142        let key = EncryptionKey::from([0xbb; 32]);
143        let input = [0x77u8; 256];
144
145        let mut via_transcrypt = [0u8; 256];
146        transcrypt(&key, 3, &input, &mut via_transcrypt).unwrap();
147
148        let mut via_in_place = input;
149        transcrypt_in_place(&key, 3, &mut via_in_place);
150
151        assert_eq!(via_transcrypt, via_in_place);
152    }
153
154    #[test]
155    fn segmented_equals_whole() {
156        let key = EncryptionKey::from([0xaa; 32]);
157        let input = [0x55u8; 128]; // 4 segments
158        let mut whole = [0u8; 128];
159        transcrypt(&key, 0, &input, &mut whole).unwrap();
160
161        // Encrypt each 32-byte segment separately with incrementing counter
162        let mut segmented = [0u8; 128];
163        for (i, chunk) in input.chunks(EncryptionKey::SIZE).enumerate() {
164            transcrypt(
165                &key,
166                i as u32,
167                chunk,
168                &mut segmented[i * EncryptionKey::SIZE..],
169            )
170            .unwrap();
171        }
172        assert_eq!(whole, segmented);
173    }
174
175    #[test]
176    fn partial_block() {
177        let key = EncryptionKey::from([0x11; 32]);
178        let input = [0xffu8; 17]; // Not aligned to 32 bytes
179        let mut output = [0u8; 17];
180        transcrypt(&key, 0, &input, &mut output).unwrap();
181
182        // Verify symmetry for partial block
183        let mut recovered = [0u8; 17];
184        transcrypt(&key, 0, &output, &mut recovered).unwrap();
185        assert_eq!(recovered, input);
186    }
187
188    #[test]
189    fn nonzero_init_ctr() {
190        let key = EncryptionKey::from([0x33; 32]);
191        let input = [0u8; 32];
192        let mut out_ctr0 = [0u8; 32];
193        let mut out_ctr5 = [0u8; 32];
194
195        transcrypt(&key, 0, &input, &mut out_ctr0).unwrap();
196        transcrypt(&key, 5, &input, &mut out_ctr5).unwrap();
197
198        assert_ne!(out_ctr0, out_ctr5);
199    }
200
201    /// Hex encoding/decoding helper for test vectors (dev-dependency only).
202    mod hex {
203        pub(super) fn decode(s: &str) -> Result<Vec<u8>, String> {
204            if !s.len().is_multiple_of(2) {
205                return Err("odd length".into());
206            }
207            (0..s.len())
208                .step_by(2)
209                .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
210                .collect()
211        }
212    }
213}