Skip to main content

ipfrs_storage/
encryption_layer.rs

1//! Storage Encryption Layer
2//!
3//! Provides a simple XOR-based encryption layer for block storage.
4//! This is for demonstration/educational purposes — not production cryptography.
5//!
6//! Supports two cipher modes:
7//! - `Xor`: Repeating key XOR
8//! - `XorWithNonce`: XOR with a key derived from nonce + base key
9
10/// Cipher mode for the encryption layer.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CipherMode {
13    /// Repeating key XOR
14    Xor,
15    /// XOR with key derived from nonce + key
16    XorWithNonce,
17}
18
19/// Configuration for the encryption layer.
20#[derive(Debug, Clone)]
21pub struct EncryptionLayerConfig {
22    /// Cipher mode to use
23    pub mode: CipherMode,
24    /// Encryption key bytes
25    pub key: Vec<u8>,
26    /// Nonce size in bytes (default 12)
27    pub nonce_size: usize,
28}
29
30impl Default for EncryptionLayerConfig {
31    fn default() -> Self {
32        Self {
33            mode: CipherMode::Xor,
34            key: vec![0u8; 32],
35            nonce_size: 12,
36        }
37    }
38}
39
40/// An encrypted block with metadata.
41#[derive(Debug, Clone)]
42pub struct EncryptedBlock {
43    /// Content identifier
44    pub cid: String,
45    /// Encrypted data
46    pub ciphertext: Vec<u8>,
47    /// Nonce used for XorWithNonce mode
48    pub nonce: Option<Vec<u8>>,
49    /// Original plaintext size
50    pub original_size: usize,
51}
52
53/// Statistics for the encryption layer.
54#[derive(Debug, Clone)]
55pub struct EncryptionLayerStats {
56    /// Number of blocks encrypted
57    pub blocks_encrypted: u64,
58    /// Number of blocks decrypted
59    pub blocks_decrypted: u64,
60    /// Total bytes encrypted
61    pub bytes_encrypted: u64,
62    /// Total bytes decrypted
63    pub bytes_decrypted: u64,
64}
65
66/// Storage encryption layer providing XOR-based encryption for blocks.
67///
68/// This is an educational/demonstration implementation. Do not use for
69/// production security.
70pub struct StorageEncryptionLayer {
71    config: EncryptionLayerConfig,
72    blocks_encrypted: u64,
73    blocks_decrypted: u64,
74    bytes_encrypted: u64,
75    bytes_decrypted: u64,
76}
77
78impl StorageEncryptionLayer {
79    /// Create a new encryption layer with the given configuration.
80    pub fn new(config: EncryptionLayerConfig) -> Self {
81        Self {
82            config,
83            blocks_encrypted: 0,
84            blocks_decrypted: 0,
85            bytes_encrypted: 0,
86            bytes_decrypted: 0,
87        }
88    }
89
90    /// Encrypt plaintext data for a given CID.
91    ///
92    /// For `Xor` mode, XORs plaintext with the repeating key.
93    /// For `XorWithNonce` mode, generates a deterministic nonce from the CID,
94    /// derives a working key by XORing the base key with the repeated nonce,
95    /// then XORs the plaintext with the working key.
96    pub fn encrypt(&mut self, cid: &str, plaintext: &[u8]) -> EncryptedBlock {
97        let (ciphertext, nonce) = match self.config.mode {
98            CipherMode::Xor => {
99                let ct = xor_with_repeating_key(plaintext, &self.config.key);
100                (ct, None)
101            }
102            CipherMode::XorWithNonce => {
103                let nonce = Self::generate_nonce(cid, self.config.nonce_size);
104                let working_key = Self::derive_key(&self.config.key, &nonce);
105                let ct = xor_with_repeating_key(plaintext, &working_key);
106                (ct, Some(nonce))
107            }
108        };
109
110        self.blocks_encrypted += 1;
111        self.bytes_encrypted += plaintext.len() as u64;
112
113        EncryptedBlock {
114            cid: cid.to_string(),
115            ciphertext,
116            nonce,
117            original_size: plaintext.len(),
118        }
119    }
120
121    /// Decrypt an encrypted block, returning the original plaintext.
122    ///
123    /// Returns an error if the decrypted size does not match `original_size`.
124    pub fn decrypt(&mut self, block: &EncryptedBlock) -> Result<Vec<u8>, String> {
125        if block.ciphertext.len() != block.original_size {
126            return Err(format!(
127                "ciphertext length {} does not match original_size {}",
128                block.ciphertext.len(),
129                block.original_size
130            ));
131        }
132
133        let plaintext = match self.config.mode {
134            CipherMode::Xor => xor_with_repeating_key(&block.ciphertext, &self.config.key),
135            CipherMode::XorWithNonce => {
136                let nonce = block.nonce.as_ref().ok_or_else(|| {
137                    "XorWithNonce mode requires a nonce in the encrypted block".to_string()
138                })?;
139                let working_key = Self::derive_key(&self.config.key, nonce);
140                xor_with_repeating_key(&block.ciphertext, &working_key)
141            }
142        };
143
144        self.blocks_decrypted += 1;
145        self.bytes_decrypted += plaintext.len() as u64;
146
147        Ok(plaintext)
148    }
149
150    /// Derive a working key by XORing the base key with a repeated nonce.
151    ///
152    /// The output length matches the base key length. The nonce is repeated
153    /// cyclically to cover the full key length.
154    pub fn derive_key(base_key: &[u8], nonce: &[u8]) -> Vec<u8> {
155        if nonce.is_empty() {
156            return base_key.to_vec();
157        }
158        base_key
159            .iter()
160            .enumerate()
161            .map(|(i, &b)| b ^ nonce[i % nonce.len()])
162            .collect()
163    }
164
165    /// Generate a deterministic nonce from a CID using FNV-1a hash.
166    ///
167    /// Produces `size` bytes by repeatedly hashing the CID with different
168    /// seed offsets derived from FNV-1a.
169    pub fn generate_nonce(cid: &str, size: usize) -> Vec<u8> {
170        let mut nonce = Vec::with_capacity(size);
171        let cid_bytes = cid.as_bytes();
172
173        // Generate nonce bytes using FNV-1a with varying seeds
174        let mut remaining = size;
175        let mut round: u64 = 0;
176        while remaining > 0 {
177            let hash = fnv1a_with_seed(cid_bytes, round);
178            let hash_bytes = hash.to_le_bytes();
179            let take = remaining.min(hash_bytes.len());
180            nonce.extend_from_slice(&hash_bytes[..take]);
181            remaining = remaining.saturating_sub(take);
182            round += 1;
183        }
184
185        nonce.truncate(size);
186        nonce
187    }
188
189    /// Heuristic check whether data appears to be encrypted.
190    ///
191    /// Estimates Shannon entropy; if > 7.5 bits/byte, data is likely
192    /// encrypted or incompressible random data.
193    pub fn is_encrypted(data: &[u8]) -> bool {
194        if data.is_empty() {
195            return false;
196        }
197
198        let mut freq = [0u64; 256];
199        for &b in data {
200            freq[b as usize] += 1;
201        }
202
203        let len = data.len() as f64;
204        let mut entropy = 0.0_f64;
205        for &count in &freq {
206            if count > 0 {
207                let p = count as f64 / len;
208                entropy -= p * p.log2();
209            }
210        }
211
212        entropy > 7.5
213    }
214
215    /// Return current encryption layer statistics.
216    pub fn stats(&self) -> EncryptionLayerStats {
217        EncryptionLayerStats {
218            blocks_encrypted: self.blocks_encrypted,
219            blocks_decrypted: self.blocks_decrypted,
220            bytes_encrypted: self.bytes_encrypted,
221            bytes_decrypted: self.bytes_decrypted,
222        }
223    }
224}
225
226/// FNV-1a hash with a seed offset for nonce generation.
227fn fnv1a_with_seed(data: &[u8], seed: u64) -> u64 {
228    let mut hash: u64 = 0xcbf29ce484222325_u64.wrapping_add(seed.wrapping_mul(0x100000001b3));
229    for &byte in data {
230        hash ^= byte as u64;
231        hash = hash.wrapping_mul(0x100000001b3);
232    }
233    hash
234}
235
236/// XOR data with a repeating key.
237fn xor_with_repeating_key(data: &[u8], key: &[u8]) -> Vec<u8> {
238    if key.is_empty() {
239        return data.to_vec();
240    }
241    data.iter()
242        .enumerate()
243        .map(|(i, &b)| b ^ key[i % key.len()])
244        .collect()
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    fn make_config(mode: CipherMode) -> EncryptionLayerConfig {
252        EncryptionLayerConfig {
253            mode,
254            key: vec![0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x9A],
255            nonce_size: 12,
256        }
257    }
258
259    #[test]
260    fn test_encrypt_decrypt_roundtrip_xor() {
261        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
262        let plaintext = b"Hello, IPFRS storage encryption!";
263        let encrypted = layer.encrypt("QmTest1", plaintext);
264        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
265        assert_eq!(decrypted, plaintext);
266    }
267
268    #[test]
269    fn test_encrypt_decrypt_roundtrip_xor_with_nonce() {
270        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::XorWithNonce));
271        let plaintext = b"Hello, IPFRS storage encryption with nonce!";
272        let encrypted = layer.encrypt("QmTest2", plaintext);
273        assert!(encrypted.nonce.is_some());
274        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
275        assert_eq!(decrypted, plaintext);
276    }
277
278    #[test]
279    fn test_xor_mode_correctness() {
280        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
281        let plaintext = vec![0x00, 0xFF, 0x55, 0xAA];
282        let encrypted = layer.encrypt("QmXor", &plaintext);
283        // XOR with key: 0x00^0xAB=0xAB, 0xFF^0xCD=0x32, 0x55^0xEF=0xBA, 0xAA^0x12=0xB8
284        assert_eq!(encrypted.ciphertext, vec![0xAB, 0x32, 0xBA, 0xB8]);
285    }
286
287    #[test]
288    fn test_ciphertext_differs_from_plaintext() {
289        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
290        let plaintext = b"This should be encrypted";
291        let encrypted = layer.encrypt("QmDiff", plaintext);
292        assert_ne!(encrypted.ciphertext, plaintext);
293    }
294
295    #[test]
296    fn test_different_keys_different_ciphertext() {
297        let config1 = EncryptionLayerConfig {
298            mode: CipherMode::Xor,
299            key: vec![0x01, 0x02, 0x03, 0x04],
300            nonce_size: 12,
301        };
302        let config2 = EncryptionLayerConfig {
303            mode: CipherMode::Xor,
304            key: vec![0x05, 0x06, 0x07, 0x08],
305            nonce_size: 12,
306        };
307        let mut layer1 = StorageEncryptionLayer::new(config1);
308        let mut layer2 = StorageEncryptionLayer::new(config2);
309        let plaintext = b"Same plaintext, different keys";
310        let enc1 = layer1.encrypt("QmKeys", plaintext);
311        let enc2 = layer2.encrypt("QmKeys", plaintext);
312        assert_ne!(enc1.ciphertext, enc2.ciphertext);
313    }
314
315    #[test]
316    fn test_deterministic_nonce_from_cid() {
317        let nonce1 = StorageEncryptionLayer::generate_nonce("QmDeterministic", 12);
318        let nonce2 = StorageEncryptionLayer::generate_nonce("QmDeterministic", 12);
319        assert_eq!(nonce1, nonce2);
320        assert_eq!(nonce1.len(), 12);
321    }
322
323    #[test]
324    fn test_different_cids_different_nonces() {
325        let nonce1 = StorageEncryptionLayer::generate_nonce("QmCid1", 12);
326        let nonce2 = StorageEncryptionLayer::generate_nonce("QmCid2", 12);
327        assert_ne!(nonce1, nonce2);
328    }
329
330    #[test]
331    fn test_derive_key() {
332        let base_key = vec![0xAA, 0xBB, 0xCC, 0xDD];
333        let nonce = vec![0x11, 0x22];
334        let derived = StorageEncryptionLayer::derive_key(&base_key, &nonce);
335        // 0xAA^0x11=0xBB, 0xBB^0x22=0x99, 0xCC^0x11=0xDD, 0xDD^0x22=0xFF
336        assert_eq!(derived, vec![0xBB, 0x99, 0xDD, 0xFF]);
337    }
338
339    #[test]
340    fn test_derive_key_empty_nonce() {
341        let base_key = vec![0xAA, 0xBB, 0xCC];
342        let derived = StorageEncryptionLayer::derive_key(&base_key, &[]);
343        assert_eq!(derived, base_key);
344    }
345
346    #[test]
347    fn test_empty_plaintext() {
348        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
349        let encrypted = layer.encrypt("QmEmpty", b"");
350        assert!(encrypted.ciphertext.is_empty());
351        assert_eq!(encrypted.original_size, 0);
352        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
353        assert!(decrypted.is_empty());
354    }
355
356    #[test]
357    fn test_empty_plaintext_with_nonce() {
358        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::XorWithNonce));
359        let encrypted = layer.encrypt("QmEmptyNonce", b"");
360        assert!(encrypted.ciphertext.is_empty());
361        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
362        assert!(decrypted.is_empty());
363    }
364
365    #[test]
366    fn test_stats_tracking() {
367        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
368        let s = layer.stats();
369        assert_eq!(s.blocks_encrypted, 0);
370        assert_eq!(s.blocks_decrypted, 0);
371
372        let data1 = b"first block";
373        let enc1 = layer.encrypt("QmStats1", data1);
374        let data2 = b"second block data";
375        let enc2 = layer.encrypt("QmStats2", data2);
376
377        let s = layer.stats();
378        assert_eq!(s.blocks_encrypted, 2);
379        assert_eq!(s.bytes_encrypted, (data1.len() + data2.len()) as u64);
380
381        let _ = layer.decrypt(&enc1).expect("decrypt should succeed");
382        let _ = layer.decrypt(&enc2).expect("decrypt should succeed");
383
384        let s = layer.stats();
385        assert_eq!(s.blocks_decrypted, 2);
386        assert_eq!(s.bytes_decrypted, (data1.len() + data2.len()) as u64);
387    }
388
389    #[test]
390    fn test_large_block() {
391        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::XorWithNonce));
392        let plaintext: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
393        let encrypted = layer.encrypt("QmLargeBlock", &plaintext);
394        assert_eq!(encrypted.ciphertext.len(), plaintext.len());
395        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
396        assert_eq!(decrypted, plaintext);
397    }
398
399    #[test]
400    fn test_key_shorter_than_data() {
401        let config = EncryptionLayerConfig {
402            mode: CipherMode::Xor,
403            key: vec![0xFF],
404            nonce_size: 12,
405        };
406        let mut layer = StorageEncryptionLayer::new(config);
407        let plaintext = vec![0x00, 0x01, 0x02, 0x03, 0x04];
408        let encrypted = layer.encrypt("QmShortKey", &plaintext);
409        // Each byte XORed with 0xFF
410        assert_eq!(encrypted.ciphertext, vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB]);
411        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
412        assert_eq!(decrypted, plaintext);
413    }
414
415    #[test]
416    fn test_same_cid_same_nonce() {
417        let nonce_a = StorageEncryptionLayer::generate_nonce("QmSameCid", 16);
418        let nonce_b = StorageEncryptionLayer::generate_nonce("QmSameCid", 16);
419        assert_eq!(nonce_a, nonce_b);
420    }
421
422    #[test]
423    fn test_is_encrypted_random_data() {
424        // Generate high-entropy data
425        let data: Vec<u8> = (0..1000)
426            .map(|i: u64| {
427                let h = fnv1a_with_seed(&i.to_le_bytes(), 42);
428                (h & 0xFF) as u8
429            })
430            .collect();
431        assert!(StorageEncryptionLayer::is_encrypted(&data));
432    }
433
434    #[test]
435    fn test_is_encrypted_low_entropy() {
436        // Repetitive data has low entropy
437        let data = vec![0xAA; 1000];
438        assert!(!StorageEncryptionLayer::is_encrypted(&data));
439    }
440
441    #[test]
442    fn test_is_encrypted_empty() {
443        assert!(!StorageEncryptionLayer::is_encrypted(&[]));
444    }
445
446    #[test]
447    fn test_decrypt_size_mismatch() {
448        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
449        let bad_block = EncryptedBlock {
450            cid: "QmBad".to_string(),
451            ciphertext: vec![0x01, 0x02, 0x03],
452            nonce: None,
453            original_size: 5, // mismatch
454        };
455        let result = layer.decrypt(&bad_block);
456        assert!(result.is_err());
457    }
458
459    #[test]
460    fn test_decrypt_xor_with_nonce_missing_nonce() {
461        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::XorWithNonce));
462        let bad_block = EncryptedBlock {
463            cid: "QmNoNonce".to_string(),
464            ciphertext: vec![0x01, 0x02],
465            nonce: None, // missing
466            original_size: 2,
467        };
468        let result = layer.decrypt(&bad_block);
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_xor_with_nonce_different_cids_different_ciphertext() {
474        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::XorWithNonce));
475        let plaintext = b"Identical data for different CIDs";
476        let enc1 = layer.encrypt("QmCidA", plaintext);
477        let enc2 = layer.encrypt("QmCidB", plaintext);
478        assert_ne!(enc1.ciphertext, enc2.ciphertext);
479    }
480
481    #[test]
482    fn test_encrypted_block_cid_preserved() {
483        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
484        let cid = "QmPreservedCid12345";
485        let encrypted = layer.encrypt(cid, b"data");
486        assert_eq!(encrypted.cid, cid);
487    }
488
489    #[test]
490    fn test_generate_nonce_various_sizes() {
491        for size in [0, 1, 8, 12, 16, 32, 64] {
492            let nonce = StorageEncryptionLayer::generate_nonce("QmVarySize", size);
493            assert_eq!(nonce.len(), size);
494        }
495    }
496
497    #[test]
498    fn test_xor_self_inverse() {
499        // XOR is its own inverse: encrypt(encrypt(x)) == x
500        let key = vec![0x42, 0x73, 0x99];
501        let data = b"self inverse test data";
502        let encrypted = xor_with_repeating_key(data, &key);
503        let decrypted = xor_with_repeating_key(&encrypted, &key);
504        assert_eq!(decrypted, data);
505    }
506
507    #[test]
508    fn test_xor_with_empty_key() {
509        let data = b"no encryption";
510        let result = xor_with_repeating_key(data, &[]);
511        assert_eq!(result, data);
512    }
513
514    #[test]
515    fn test_default_config() {
516        let config = EncryptionLayerConfig::default();
517        assert_eq!(config.mode, CipherMode::Xor);
518        assert_eq!(config.key.len(), 32);
519        assert_eq!(config.nonce_size, 12);
520    }
521
522    #[test]
523    fn test_stats_initial_zeros() {
524        let layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
525        let s = layer.stats();
526        assert_eq!(s.blocks_encrypted, 0);
527        assert_eq!(s.blocks_decrypted, 0);
528        assert_eq!(s.bytes_encrypted, 0);
529        assert_eq!(s.bytes_decrypted, 0);
530    }
531
532    #[test]
533    fn test_single_byte_data() {
534        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::XorWithNonce));
535        let plaintext = &[0x42];
536        let encrypted = layer.encrypt("QmSingleByte", plaintext);
537        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
538        assert_eq!(decrypted, plaintext);
539    }
540
541    #[test]
542    fn test_all_byte_values() {
543        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
544        let plaintext: Vec<u8> = (0..=255).collect();
545        let encrypted = layer.encrypt("QmAllBytes", &plaintext);
546        let decrypted = layer.decrypt(&encrypted).expect("decrypt should succeed");
547        assert_eq!(decrypted, plaintext);
548    }
549
550    #[test]
551    fn test_temp_dir_usage() {
552        // Demonstrate use of temp_dir per policy (write encrypted data to temp file)
553        let tmp = std::env::temp_dir().join("ipfrs_encryption_layer_test");
554        let mut layer = StorageEncryptionLayer::new(make_config(CipherMode::Xor));
555        let plaintext = b"temp dir test";
556        let encrypted = layer.encrypt("QmTmpDir", plaintext);
557        std::fs::write(&tmp, &encrypted.ciphertext).expect("write to temp should succeed");
558        let read_back = std::fs::read(&tmp).expect("read from temp should succeed");
559        assert_eq!(read_back, encrypted.ciphertext);
560        let _ = std::fs::remove_file(&tmp);
561    }
562}