Skip to main content

self_encryption/
utils.rs

1use crate::cipher::{Key, Nonce, Pad, KEY_SIZE, NONCE_SIZE, PAD_SIZE};
2use bytes::Bytes;
3use xor_name::XorName;
4
5/// Helper function to XOR a data with a pad (pad will be rotated to fill the length)
6pub(crate) fn xor(data: &Bytes, &Pad(pad): &Pad) -> Bytes {
7    let vec: Vec<_> = data
8        .iter()
9        .zip(pad.iter().cycle())
10        .map(|(&a, &b)| a ^ b)
11        .collect();
12    Bytes::from(vec)
13}
14
15pub fn extract_hashes(data_map: &crate::DataMap) -> Vec<XorName> {
16    data_map.infos().iter().map(|c| c.src_hash).collect()
17}
18
19pub(crate) fn get_pad_key_and_nonce(
20    chunk_index: usize,
21    chunk_hashes: &[XorName],
22    child_level: usize,
23) -> crate::Result<(Pad, Key, Nonce)> {
24    let (n_1, n_2) = get_n_1_n_2(chunk_index, chunk_hashes.len())?;
25
26    let src_hash = chunk_hashes
27        .get(chunk_index)
28        .ok_or_else(|| crate::Error::Generic(format!("chunk_index {chunk_index} out of bounds")))?;
29    let n_1_src_hash = chunk_hashes
30        .get(n_1)
31        .ok_or_else(|| crate::Error::Generic(format!("n_1 index {n_1} out of bounds")))?;
32    let n_2_src_hash = chunk_hashes
33        .get(n_2)
34        .ok_or_else(|| crate::Error::Generic(format!("n_2 index {n_2} out of bounds")))?;
35
36    Ok(get_pki(
37        src_hash,
38        n_1_src_hash,
39        n_2_src_hash,
40        chunk_index,
41        child_level,
42    ))
43}
44
45pub(crate) fn get_n_1_n_2(
46    chunk_index: usize,
47    total_num_chunks: usize,
48) -> crate::Result<(usize, usize)> {
49    if total_num_chunks < 3 {
50        return Err(crate::Error::Generic(format!(
51            "total_num_chunks must be at least 3, got {total_num_chunks}"
52        )));
53    }
54    if chunk_index >= total_num_chunks {
55        return Err(crate::Error::Generic(format!(
56            "chunk_index {chunk_index} out of bounds for total_num_chunks {total_num_chunks}"
57        )));
58    }
59    match chunk_index {
60        0 => Ok((total_num_chunks - 1, total_num_chunks - 2)),
61        1 => Ok((0, total_num_chunks - 1)),
62        n => Ok((n - 1, n - 2)),
63    }
64}
65
66pub(crate) fn get_pki(
67    src_hash: &XorName,
68    n_1_src_hash: &XorName,
69    n_2_src_hash: &XorName,
70    chunk_index: usize,
71    child_level: usize,
72) -> (Pad, Key, Nonce) {
73    // Domain-separated BLAKE3 KDF with full chunk context.
74    // Including src_hash ensures that two different chunks sharing the same
75    // predecessors (n_1, n_2) will derive different (key, nonce) pairs,
76    // eliminating the nonce-reuse hazard in ChaCha20-Poly1305.
77    let mut context_material = Vec::with_capacity(32 + 32 + 32 + 8 + 8);
78    context_material.extend_from_slice(&src_hash.0);
79    context_material.extend_from_slice(&n_1_src_hash.0);
80    context_material.extend_from_slice(&n_2_src_hash.0);
81    context_material.extend_from_slice(&(chunk_index as u64).to_le_bytes());
82    context_material.extend_from_slice(&(child_level as u64).to_le_bytes());
83
84    let mut output = [0u8; PAD_SIZE + KEY_SIZE + NONCE_SIZE];
85    let mut hasher = blake3::Hasher::new_derive_key("self_encryption/chunk/v2");
86    let _ = hasher.update(&context_material);
87    let mut output_reader = hasher.finalize_xof();
88    output_reader.fill(&mut output);
89
90    let mut pad = [0u8; PAD_SIZE];
91    let mut key = [0u8; KEY_SIZE];
92    let mut nonce = [0u8; NONCE_SIZE];
93
94    pad.copy_from_slice(&output[..PAD_SIZE]);
95    key.copy_from_slice(&output[PAD_SIZE..PAD_SIZE + KEY_SIZE]);
96    nonce.copy_from_slice(&output[PAD_SIZE + KEY_SIZE..]);
97
98    (Pad(pad), Key(key), Nonce(nonce))
99}
100
101// Returns the number of chunks according to file size.
102pub(crate) fn get_num_chunks(file_size: usize) -> usize {
103    get_num_chunks_with_variable_max(file_size, crate::MAX_CHUNK_SIZE)
104}
105
106// Returns the number of chunks according to file size.
107pub(crate) fn get_num_chunks_with_variable_max(file_size: usize, max_chunk_size: usize) -> usize {
108    if file_size < (3 * crate::MIN_CHUNK_SIZE) {
109        return 0;
110    }
111    if file_size < (3 * max_chunk_size) {
112        return 3;
113    }
114    if file_size.is_multiple_of(max_chunk_size) {
115        file_size / max_chunk_size
116    } else {
117        (file_size / max_chunk_size) + 1
118    }
119}
120
121// Returns the size of a chunk according to file size.
122pub(crate) fn get_chunk_size(file_size: usize, chunk_index: usize) -> usize {
123    get_chunk_size_with_variable_max(file_size, chunk_index, crate::MAX_CHUNK_SIZE)
124}
125
126// Returns the size of a chunk according to file size.
127pub(crate) fn get_chunk_size_with_variable_max(
128    file_size: usize,
129    chunk_index: usize,
130    max_chunk_size: usize,
131) -> usize {
132    if file_size < 3 * crate::MIN_CHUNK_SIZE {
133        return 0;
134    }
135    if file_size < 3 * max_chunk_size {
136        if chunk_index < 2 {
137            return file_size / 3;
138        } else {
139            // When the file_size % 3 > 0, the third (last) chunk includes the remainder
140            return file_size - (2 * (file_size / 3));
141        }
142    }
143    let total_chunks = get_num_chunks_with_variable_max(file_size, max_chunk_size);
144    if chunk_index < total_chunks - 2 {
145        return max_chunk_size;
146    }
147    let remainder = file_size % max_chunk_size;
148    let penultimate = (total_chunks - 2) == chunk_index;
149    if remainder == 0 {
150        return max_chunk_size;
151    }
152    if remainder < crate::MIN_CHUNK_SIZE {
153        if penultimate {
154            max_chunk_size - crate::MIN_CHUNK_SIZE
155        } else {
156            crate::MIN_CHUNK_SIZE + remainder
157        }
158    } else if penultimate {
159        max_chunk_size
160    } else {
161        remainder
162    }
163}
164
165// Returns the [start, end) half-open byte range of a chunk.
166pub(crate) fn get_start_end_positions(file_size: usize, chunk_index: usize) -> (usize, usize) {
167    if get_num_chunks(file_size) == 0 {
168        return (0, 0);
169    }
170    let start = get_start_position(file_size, chunk_index);
171    (start, start + get_chunk_size(file_size, chunk_index))
172}
173
174pub(crate) fn get_start_position(file_size: usize, chunk_index: usize) -> usize {
175    let total_chunks = get_num_chunks(file_size);
176    if total_chunks == 0 {
177        return 0;
178    }
179    let last = (total_chunks - 1) == chunk_index;
180    let first_chunk_size = get_chunk_size(file_size, 0);
181    if last {
182        first_chunk_size * (chunk_index - 1) + get_chunk_size(file_size, chunk_index - 1)
183    } else {
184        first_chunk_size * chunk_index
185    }
186}
187
188#[allow(dead_code)]
189pub(crate) fn get_chunk_index(file_size: usize, position: usize) -> usize {
190    let num_chunks = get_num_chunks(file_size);
191    if num_chunks == 0 {
192        return 0; // FIX THIS SHOULD NOT BE ALLOWED
193    }
194
195    let chunk_size = get_chunk_size(file_size, 0);
196    let remainder = file_size % chunk_size;
197
198    if remainder == 0
199        || remainder >= crate::MIN_CHUNK_SIZE
200        || position < file_size - remainder - crate::MIN_CHUNK_SIZE
201    {
202        usize::min(position / chunk_size, num_chunks - 1)
203    } else {
204        num_chunks - 1
205    }
206}
207
208#[cfg(test)]
209#[allow(clippy::unwrap_used, clippy::expect_used)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_different_chunks_same_predecessors_yield_different_key_nonce() {
215        let n1 = XorName([0xAA; 32]);
216        let n2 = XorName([0xBB; 32]);
217
218        let src_hash_a = XorName([0x01; 32]);
219        let src_hash_b = XorName([0x02; 32]);
220
221        let (pad_a, key_a, nonce_a) = get_pki(&src_hash_a, &n1, &n2, 0, 0);
222        let (pad_b, key_b, nonce_b) = get_pki(&src_hash_b, &n1, &n2, 0, 0);
223
224        assert_ne!(
225            key_a.0, key_b.0,
226            "different src_hash must produce different keys"
227        );
228        assert_ne!(
229            nonce_a.0, nonce_b.0,
230            "different src_hash must produce different nonces"
231        );
232        assert_ne!(
233            pad_a.0, pad_b.0,
234            "different src_hash must produce different pads"
235        );
236    }
237
238    #[test]
239    fn test_kdf_domain_separation_by_chunk_index() {
240        let src = XorName([0x11; 32]);
241        let n1 = XorName([0x22; 32]);
242        let n2 = XorName([0x33; 32]);
243
244        let (_, key_0, nonce_0) = get_pki(&src, &n1, &n2, 0, 0);
245        let (_, key_1, nonce_1) = get_pki(&src, &n1, &n2, 1, 0);
246
247        assert_ne!(
248            key_0.0, key_1.0,
249            "different chunk_index must produce different keys"
250        );
251        assert_ne!(
252            nonce_0.0, nonce_1.0,
253            "different chunk_index must produce different nonces"
254        );
255    }
256
257    #[test]
258    fn test_kdf_domain_separation_by_child_level() {
259        let src = XorName([0x11; 32]);
260        let n1 = XorName([0x22; 32]);
261        let n2 = XorName([0x33; 32]);
262
263        let (_, key_c0, nonce_c0) = get_pki(&src, &n1, &n2, 0, 0);
264        let (_, key_c1, nonce_c1) = get_pki(&src, &n1, &n2, 0, 1);
265
266        assert_ne!(
267            key_c0.0, key_c1.0,
268            "different child_level must produce different keys"
269        );
270        assert_ne!(
271            nonce_c0.0, nonce_c1.0,
272            "different child_level must produce different nonces"
273        );
274    }
275
276    #[test]
277    fn test_kdf_deterministic() {
278        let src = XorName([0x42; 32]);
279        let n1 = XorName([0xAA; 32]);
280        let n2 = XorName([0xBB; 32]);
281
282        let (pad_a, key_a, nonce_a) = get_pki(&src, &n1, &n2, 5, 2);
283        let (pad_b, key_b, nonce_b) = get_pki(&src, &n1, &n2, 5, 2);
284
285        assert_eq!(pad_a.0, pad_b.0, "same inputs must produce identical pads");
286        assert_eq!(key_a.0, key_b.0, "same inputs must produce identical keys");
287        assert_eq!(
288            nonce_a.0, nonce_b.0,
289            "same inputs must produce identical nonces"
290        );
291    }
292
293    #[test]
294    fn test_kdf_avalanche_single_bit_flip() {
295        let src_a = [0x42u8; 32];
296        let src_b = {
297            let mut b = src_a;
298            b[0] ^= 0x01; // flip one bit
299            b
300        };
301
302        let n1 = XorName([0xAA; 32]);
303        let n2 = XorName([0xBB; 32]);
304
305        let (pad_a, key_a, nonce_a) = get_pki(&XorName(src_a), &n1, &n2, 0, 0);
306        let (pad_b, key_b, nonce_b) = get_pki(&XorName(src_b), &n1, &n2, 0, 0);
307
308        // Count differing bytes — expect at least 30% differ (avalanche property)
309        let key_diff = key_a
310            .0
311            .iter()
312            .zip(key_b.0.iter())
313            .filter(|(a, b)| a != b)
314            .count();
315        let nonce_diff = nonce_a
316            .0
317            .iter()
318            .zip(nonce_b.0.iter())
319            .filter(|(a, b)| a != b)
320            .count();
321        let pad_diff = pad_a
322            .0
323            .iter()
324            .zip(pad_b.0.iter())
325            .filter(|(a, b)| a != b)
326            .count();
327
328        assert!(
329            key_diff >= 10,
330            "expected at least 10/32 key bytes to differ, got {}",
331            key_diff
332        );
333        assert!(
334            nonce_diff >= 4,
335            "expected at least 4/12 nonce bytes to differ, got {}",
336            nonce_diff
337        );
338        assert!(
339            pad_diff >= 15,
340            "expected at least 15/52 pad bytes to differ, got {}",
341            pad_diff
342        );
343    }
344
345    #[test]
346    fn test_get_n_1_n_2_out_of_bounds() {
347        let result = get_n_1_n_2(5, 3);
348        assert!(result.is_err(), "chunk_index >= total should fail");
349        let result = get_n_1_n_2(3, 3);
350        assert!(result.is_err(), "chunk_index == total should fail");
351        let result = get_n_1_n_2(2, 3);
352        assert!(result.is_ok(), "chunk_index < total should succeed");
353    }
354
355    #[test]
356    fn test_kdf_output_sizes() {
357        let src = XorName([0x01; 32]);
358        let n1 = XorName([0x02; 32]);
359        let n2 = XorName([0x03; 32]);
360
361        let (pad, key, nonce) = get_pki(&src, &n1, &n2, 0, 0);
362
363        assert_eq!(pad.0.len(), PAD_SIZE, "pad must be {PAD_SIZE} bytes");
364        assert_eq!(key.0.len(), KEY_SIZE, "key must be {KEY_SIZE} bytes");
365        assert_eq!(
366            nonce.0.len(),
367            NONCE_SIZE,
368            "nonce must be {NONCE_SIZE} bytes"
369        );
370    }
371}