chie_core/
chunk_encryption.rs

1//! Chunk-level encryption with per-chunk nonces for CHIE Protocol.
2//!
3//! This module provides:
4//! - Per-chunk encryption with unique nonces
5//! - Chunk key derivation from master key
6//! - Streaming chunk encryption/decryption
7
8use chie_crypto::{
9    Hash, decrypt, derive_chunk_nonce as crypto_derive_nonce, derive_content_key, encrypt, hash,
10};
11use std::io::{self, Read, Write};
12use thiserror::Error;
13
14/// Default chunk size for encryption (256 KB).
15pub const ENCRYPTED_CHUNK_SIZE: usize = 262_144;
16
17/// Nonce size (12 bytes for ChaCha20-Poly1305).
18pub const NONCE_SIZE: usize = 12;
19
20/// Chunk encryption error.
21#[derive(Debug, Error)]
22pub enum ChunkEncryptionError {
23    #[error("Encryption failed: {0}")]
24    EncryptionFailed(String),
25
26    #[error("Decryption failed: {0}")]
27    DecryptionFailed(String),
28
29    #[error("Invalid chunk index")]
30    InvalidChunkIndex,
31
32    #[error("Key derivation failed: {0}")]
33    KeyDerivationFailed(String),
34
35    #[error("Invalid nonce")]
36    InvalidNonce,
37
38    #[error("IO error: {0}")]
39    IoError(#[from] io::Error),
40}
41
42/// Derive a deterministic nonce for a chunk.
43#[inline]
44pub fn derive_chunk_nonce(
45    master_key: &[u8; 32],
46    content_id: &str,
47    chunk_index: u64,
48) -> Result<[u8; NONCE_SIZE], ChunkEncryptionError> {
49    crypto_derive_nonce(master_key, content_id, chunk_index)
50        .map_err(|e| ChunkEncryptionError::KeyDerivationFailed(e.to_string()))
51}
52
53/// Derive a chunk-specific key from the master key.
54#[inline]
55pub fn derive_chunk_key(
56    master_key: &[u8; 32],
57    content_id: &str,
58    chunk_index: u64,
59) -> Result<[u8; 32], ChunkEncryptionError> {
60    derive_content_key(master_key, content_id, chunk_index)
61        .map_err(|e| ChunkEncryptionError::KeyDerivationFailed(e.to_string()))
62}
63
64/// Encrypted chunk with metadata.
65#[derive(Debug, Clone)]
66pub struct EncryptedChunk {
67    /// Chunk index.
68    pub index: u64,
69    /// Encrypted data.
70    pub ciphertext: Vec<u8>,
71    /// Nonce used for encryption.
72    pub nonce: [u8; NONCE_SIZE],
73    /// Hash of original plaintext.
74    pub plaintext_hash: Hash,
75}
76
77impl EncryptedChunk {
78    /// Get the size of the encrypted data.
79    #[must_use]
80    #[inline]
81    pub fn size(&self) -> usize {
82        self.ciphertext.len()
83    }
84
85    /// Serialize to bytes.
86    #[must_use]
87    #[inline]
88    pub fn to_bytes(&self) -> Vec<u8> {
89        let mut bytes = Vec::new();
90        bytes.extend_from_slice(&self.index.to_le_bytes());
91        bytes.extend_from_slice(&self.nonce);
92        bytes.extend_from_slice(&self.plaintext_hash);
93        bytes.extend_from_slice(&(self.ciphertext.len() as u32).to_le_bytes());
94        bytes.extend_from_slice(&self.ciphertext);
95        bytes
96    }
97
98    /// Deserialize from bytes.
99    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ChunkEncryptionError> {
100        if bytes.len() < 8 + NONCE_SIZE + 32 + 4 {
101            return Err(ChunkEncryptionError::InvalidNonce);
102        }
103
104        let index = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
105        let mut nonce = [0u8; NONCE_SIZE];
106        nonce.copy_from_slice(&bytes[8..8 + NONCE_SIZE]);
107        let mut plaintext_hash = [0u8; 32];
108        plaintext_hash.copy_from_slice(&bytes[8 + NONCE_SIZE..8 + NONCE_SIZE + 32]);
109        let ciphertext_len = u32::from_le_bytes(
110            bytes[8 + NONCE_SIZE + 32..8 + NONCE_SIZE + 36]
111                .try_into()
112                .unwrap(),
113        ) as usize;
114
115        if bytes.len() < 8 + NONCE_SIZE + 36 + ciphertext_len {
116            return Err(ChunkEncryptionError::InvalidNonce);
117        }
118
119        let ciphertext = bytes[8 + NONCE_SIZE + 36..8 + NONCE_SIZE + 36 + ciphertext_len].to_vec();
120
121        Ok(Self {
122            index,
123            ciphertext,
124            nonce,
125            plaintext_hash,
126        })
127    }
128}
129
130/// Chunk encryptor for streaming encryption.
131pub struct ChunkEncryptor {
132    master_key: [u8; 32],
133    content_id: String,
134    chunk_size: usize,
135    current_index: u64,
136}
137
138impl ChunkEncryptor {
139    /// Create a new chunk encryptor.
140    #[must_use]
141    #[inline]
142    pub fn new(master_key: [u8; 32], content_id: impl Into<String>, chunk_size: usize) -> Self {
143        Self {
144            master_key,
145            content_id: content_id.into(),
146            chunk_size,
147            current_index: 0,
148        }
149    }
150
151    /// Encrypt a chunk of data.
152    pub fn encrypt_chunk(
153        &mut self,
154        plaintext: &[u8],
155    ) -> Result<EncryptedChunk, ChunkEncryptionError> {
156        let index = self.current_index;
157        self.current_index += 1;
158
159        encrypt_chunk_with_index(&self.master_key, &self.content_id, index, plaintext)
160    }
161
162    /// Get the current chunk index.
163    #[must_use]
164    #[inline]
165    pub const fn current_index(&self) -> u64 {
166        self.current_index
167    }
168
169    /// Get the chunk size.
170    #[must_use]
171    #[inline]
172    pub const fn chunk_size(&self) -> usize {
173        self.chunk_size
174    }
175
176    /// Reset the chunk index.
177    #[inline]
178    pub fn reset(&mut self) {
179        self.current_index = 0;
180    }
181}
182
183/// Encrypt a single chunk with a specific index.
184pub fn encrypt_chunk_with_index(
185    master_key: &[u8; 32],
186    content_id: &str,
187    chunk_index: u64,
188    plaintext: &[u8],
189) -> Result<EncryptedChunk, ChunkEncryptionError> {
190    // Derive chunk-specific key
191    let chunk_key = derive_chunk_key(master_key, content_id, chunk_index)?;
192
193    // Generate deterministic nonce
194    let nonce = derive_chunk_nonce(master_key, content_id, chunk_index)?;
195
196    // Hash plaintext for integrity verification
197    let plaintext_hash = hash(plaintext);
198
199    // Encrypt (encrypt takes: data, key, nonce)
200    let ciphertext = encrypt(plaintext, &chunk_key, &nonce)
201        .map_err(|e| ChunkEncryptionError::EncryptionFailed(e.to_string()))?;
202
203    Ok(EncryptedChunk {
204        index: chunk_index,
205        ciphertext,
206        nonce,
207        plaintext_hash,
208    })
209}
210
211/// Decrypt a single chunk.
212pub fn decrypt_chunk(
213    master_key: &[u8; 32],
214    content_id: &str,
215    chunk: &EncryptedChunk,
216) -> Result<Vec<u8>, ChunkEncryptionError> {
217    // Derive chunk-specific key
218    let chunk_key = derive_chunk_key(master_key, content_id, chunk.index)?;
219
220    // Verify nonce matches expected
221    let expected_nonce = derive_chunk_nonce(master_key, content_id, chunk.index)?;
222    if chunk.nonce != expected_nonce {
223        return Err(ChunkEncryptionError::InvalidNonce);
224    }
225
226    // Decrypt (decrypt takes: ciphertext, key, nonce)
227    let plaintext = decrypt(&chunk.ciphertext, &chunk_key, &chunk.nonce)
228        .map_err(|e| ChunkEncryptionError::DecryptionFailed(e.to_string()))?;
229
230    // Verify plaintext hash
231    let computed_hash = hash(&plaintext);
232    if computed_hash != chunk.plaintext_hash {
233        return Err(ChunkEncryptionError::DecryptionFailed(
234            "Plaintext hash mismatch".to_string(),
235        ));
236    }
237
238    Ok(plaintext)
239}
240
241/// Chunk decryptor for streaming decryption.
242pub struct ChunkDecryptor {
243    master_key: [u8; 32],
244    content_id: String,
245}
246
247impl ChunkDecryptor {
248    /// Create a new chunk decryptor.
249    #[must_use]
250    #[inline]
251    pub fn new(master_key: [u8; 32], content_id: impl Into<String>) -> Self {
252        Self {
253            master_key,
254            content_id: content_id.into(),
255        }
256    }
257
258    /// Decrypt a chunk.
259    pub fn decrypt_chunk(&self, chunk: &EncryptedChunk) -> Result<Vec<u8>, ChunkEncryptionError> {
260        decrypt_chunk(&self.master_key, &self.content_id, chunk)
261    }
262}
263
264/// Encrypt content from a reader to encrypted chunks.
265pub fn encrypt_content<R: Read>(
266    master_key: &[u8; 32],
267    content_id: &str,
268    reader: &mut R,
269    chunk_size: usize,
270) -> Result<Vec<EncryptedChunk>, ChunkEncryptionError> {
271    let mut encryptor = ChunkEncryptor::new(*master_key, content_id, chunk_size);
272    let mut chunks = Vec::new();
273    let mut buffer = vec![0u8; chunk_size];
274
275    loop {
276        let mut total_read = 0;
277
278        while total_read < chunk_size {
279            let bytes_read = reader.read(&mut buffer[total_read..])?;
280            if bytes_read == 0 {
281                break;
282            }
283            total_read += bytes_read;
284        }
285
286        if total_read == 0 {
287            break;
288        }
289
290        let chunk = encryptor.encrypt_chunk(&buffer[..total_read])?;
291        chunks.push(chunk);
292
293        if total_read < chunk_size {
294            break;
295        }
296    }
297
298    Ok(chunks)
299}
300
301/// Decrypt chunks to a writer.
302pub fn decrypt_content<W: Write>(
303    master_key: &[u8; 32],
304    content_id: &str,
305    chunks: &[EncryptedChunk],
306    writer: &mut W,
307) -> Result<u64, ChunkEncryptionError> {
308    let decryptor = ChunkDecryptor::new(*master_key, content_id);
309    let mut total_written = 0u64;
310
311    for chunk in chunks {
312        let plaintext = decryptor.decrypt_chunk(chunk)?;
313        writer.write_all(&plaintext)?;
314        total_written += plaintext.len() as u64;
315    }
316
317    Ok(total_written)
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use std::io::Cursor;
324
325    #[test]
326    fn test_derive_chunk_nonce() {
327        let master_key = [0u8; 32];
328        let content_id = "QmTest123";
329
330        let nonce1 = derive_chunk_nonce(&master_key, content_id, 0).unwrap();
331        let nonce2 = derive_chunk_nonce(&master_key, content_id, 1).unwrap();
332        let nonce3 = derive_chunk_nonce(&master_key, content_id, 0).unwrap();
333
334        assert_ne!(nonce1, nonce2); // Different indices = different nonces
335        assert_eq!(nonce1, nonce3); // Same inputs = same nonce
336    }
337
338    #[test]
339    fn test_derive_chunk_key() {
340        let master_key = [0u8; 32];
341        let content_id = "QmTest123";
342
343        let key1 = derive_chunk_key(&master_key, content_id, 0).unwrap();
344        let key2 = derive_chunk_key(&master_key, content_id, 1).unwrap();
345
346        assert_ne!(key1, key2); // Different indices = different keys
347    }
348
349    #[test]
350    fn test_encrypt_decrypt_chunk() {
351        let master_key = [1u8; 32];
352        let content_id = "QmTest123";
353        let plaintext = b"Hello, CHIE Protocol!";
354
355        let chunk = encrypt_chunk_with_index(&master_key, content_id, 0, plaintext).unwrap();
356        let decrypted = decrypt_chunk(&master_key, content_id, &chunk).unwrap();
357
358        assert_eq!(decrypted, plaintext);
359    }
360
361    #[test]
362    fn test_chunk_encryptor() {
363        let master_key = [2u8; 32];
364        let content_id = "QmTest456";
365
366        let mut encryptor = ChunkEncryptor::new(master_key, content_id, 1024);
367
368        let chunk1 = encryptor.encrypt_chunk(b"Chunk 1").unwrap();
369        let chunk2 = encryptor.encrypt_chunk(b"Chunk 2").unwrap();
370
371        assert_eq!(chunk1.index, 0);
372        assert_eq!(chunk2.index, 1);
373        assert_ne!(chunk1.nonce, chunk2.nonce);
374    }
375
376    #[test]
377    fn test_encrypt_content() {
378        let master_key = [3u8; 32];
379        let content_id = "QmContent";
380        let data = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
381        let mut cursor = Cursor::new(data);
382
383        let chunks = encrypt_content(&master_key, content_id, &mut cursor, 10).unwrap();
384
385        assert_eq!(chunks.len(), 3); // 26 bytes / 10 = 3 chunks
386
387        // Decrypt and verify
388        let mut output = Vec::new();
389        decrypt_content(&master_key, content_id, &chunks, &mut output).unwrap();
390
391        assert_eq!(output, data);
392    }
393
394    #[test]
395    fn test_encrypted_chunk_serialization() {
396        let master_key = [4u8; 32];
397        let content_id = "QmSerial";
398
399        let chunk = encrypt_chunk_with_index(&master_key, content_id, 42, b"Test data").unwrap();
400
401        let bytes = chunk.to_bytes();
402        let deserialized = EncryptedChunk::from_bytes(&bytes).unwrap();
403
404        assert_eq!(chunk.index, deserialized.index);
405        assert_eq!(chunk.nonce, deserialized.nonce);
406        assert_eq!(chunk.plaintext_hash, deserialized.plaintext_hash);
407        assert_eq!(chunk.ciphertext, deserialized.ciphertext);
408    }
409
410    #[test]
411    fn test_different_chunks_different_keys() {
412        let master_key = [5u8; 32];
413        let content_id = "QmDiffKeys";
414
415        let chunk1 = encrypt_chunk_with_index(&master_key, content_id, 0, b"Same data").unwrap();
416        let chunk2 = encrypt_chunk_with_index(&master_key, content_id, 1, b"Same data").unwrap();
417
418        // Same plaintext but different chunks should produce different ciphertext
419        assert_ne!(chunk1.ciphertext, chunk2.ciphertext);
420    }
421}