diff --git a/Cargo.lock b/Cargo.lock index 8d2d0a9e..990ffbef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ dependencies = [ "log", "parking", "polling 2.8.0", - "rustix 0.37.27", + "rustix 0.37.28", "slab", "socket2 0.4.10", "waker-fn", @@ -1953,9 +1953,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.27" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ "bitflags 1.3.2", "errno", diff --git a/ipfs_plugin/src/lib.rs b/ipfs_plugin/src/lib.rs new file mode 100644 index 00000000..5180169f --- /dev/null +++ b/ipfs_plugin/src/lib.rs @@ -0,0 +1,111 @@ +use crate::crypto::write::CryptoWrite; // Resolves the error with .finish() +use crate::crypto::{self, Cipher}; +use shush_rs::SecretVec; +use std::io::{Cursor, Read, Write, Error, ErrorKind, Result}; +use std::sync::Mutex; // Required for reusable decryption buffer + +pub struct IpfsCipher { + key: SecretVec, + cipher: Cipher, + dec_buffer: Mutex>, // Reusable buffer for decryption +} + +impl IpfsCipher { + /// Initializes the plugin with a secure key and the default cipher + pub fn new(secret_key: Vec) -> Self { + // Validate key length to prevent downstream library panics (Point 3 in review) + assert_eq!( + secret_key.len(), + 32, + "Invalid key length for ChaCha20Poly1305. Expected exactly 32 bytes." + ); + + Self { + key: SecretVec::from(secret_key), + cipher: Cipher::ChaCha20Poly1305, + dec_buffer: Mutex::new(Vec::with_capacity(1024 * 64)), // Pre-allocate 64KB + } + } + + /// Encrypts a block of data using a Cursor to simulate an in-memory file + pub fn encrypt(&self, data: &[u8]) -> Result> { + if data.is_empty() { + return Ok(Vec::new()); + } + + // Pre-allocate capacity matching data size to satisfy 'static bounds without reallocation churn + let estimated_capacity = data.len() + 256; + let memory_file = Cursor::new(Vec::with_capacity(estimated_capacity)); + let mut writer = crypto::create_write(memory_file, self.cipher, &self.key); + + writer + .write_all(data) + .map_err(|e| Error::new(ErrorKind::Other, format!("Error writing IPFS data: {:?}", e)))?; + + // .finish() returns the internal object (the Cursor) on success + let finished_cursor = writer + .finish() + .map_err(|e| Error::new(ErrorKind::Other, format!("Error finalizing IPFS encryption: {:?}", e)))?; + + // Extract the byte vector from the inner cursor + Ok(finished_cursor.into_inner()) + } + + /// Decrypts a block of data using the native CryptoRead from rencfs + pub fn decrypt(&self, encrypted_data: &[u8]) -> Result> { + if encrypted_data.is_empty() { + return Ok(Vec::new()); + } + + let mut buffer_guard = self.dec_buffer.lock().map_err(|_| { + Error::new(ErrorKind::Other, "Failed to lock decryption buffer lock") + })?; + buffer_guard.clear(); + + let mut reader = crypto::create_read(encrypted_data, self.cipher, &self.key); + + reader + .read_to_end(&mut *buffer_guard) + .map_err(|e| Error::new(ErrorKind::Other, format!("Error decrypting IPFS data: {:?}", e)))?; + + Ok(buffer_guard.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ipfs_encryption_decryption() { + // Generate a 32-byte test key (required for ChaCha20Poly1305) + let test_key = vec![0u8; 32]; + let cipher = IpfsCipher::new(test_key); + + let original_data = b"Secret data sent via IPFS with a unique CID!"; + + // 1. Encrypt the data + let encrypted = cipher.encrypt(original_data).expect("Encryption failed"); + assert_ne!( + original_data.to_vec(), + encrypted, + "Encrypted data must not match the original data" + ); + + // 2. Decrypt the data back + let decrypted = cipher.decrypt(&encrypted).expect("Decryption failed"); + assert_eq!( + original_data.to_vec(), + decrypted, + "Decrypted data does not match the original data" + ); + } + + #[test] + #[should_panic(expected = "Invalid key length")] + fn test_invalid_key_length_panics() { + // Test that an invalid key length triggers the validation assert + let short_key = vec![0u8; 16]; + let _cipher = IpfsCipher::new(short_key); + } +} \ No newline at end of file diff --git a/src/encryptedfs.rs b/src/encryptedfs.rs index 7c2da5a4..eb73e96d 100644 --- a/src/encryptedfs.rs +++ b/src/encryptedfs.rs @@ -1,3 +1,4 @@ +use crate::ipfs_plugin::IpfsCipher; use argon2::password_hash::rand_core::RngCore; use async_trait::async_trait; use futures_util::TryStreamExt; @@ -574,6 +575,7 @@ pub struct EncryptedFs { sizes_read: Mutex>, requested_read: Mutex>, read_only: bool, + pub ipfs_cipher: std::sync::RwLock>, } impl EncryptedFs { @@ -627,6 +629,7 @@ impl EncryptedFs { sizes_read: Mutex::default(), requested_read: Mutex::default(), read_only, + ipfs_cipher: std::sync::RwLock::new(None), }; let arc = Arc::new(fs); @@ -1437,6 +1440,23 @@ impl EncryptedFs { (buf, len) }; + if let Ok(ipfs_guard) = self.ipfs_cipher.read() { + if let Some(ipfs) = ipfs_guard.as_ref() { + // Process and cross-verify the requested byte chunk block via decryption + let _ipfs_decrypted_block = ipfs.decrypt(&_buf[..len]).map_err(|e| { + error!(err = %e, "IPFS plugin decryption failure"); + FsError::Io { + source: e, + backtrace: Backtrace::capture(), + } + })?; + + // FUTURE INTEGRATION HOOK: + // Transparently fallback to fetching chunks directly from the IPFS network + // peer-to-peer swarm if local block reads miss or require distributed fetching. + } + } + ctx.attr.atime = SystemTime::now(); drop(ctx); @@ -1666,6 +1686,21 @@ impl EncryptedFs { (writer.stream_position()?, len) }; + // Pass data through the IPFS plugin seamlessly if it's active + if let Ok(ipfs_guard) = self.ipfs_cipher.read() { + if let Some(ipfs) = ipfs_guard.as_ref() { + // Intercept the exact chunk slice written to disk + let _ipfs_encrypted_block = ipfs.encrypt(&buf[..len]).map_err(|e| { + error!(err = %e, "IPFS plugin encryption failure"); + FsError::Io { + source: e, + backtrace: Backtrace::capture(), + } + })?; + // Future integration hook: stream `_ipfs_encrypted_block` directly to the Kubo RPC client + } + } + // let size = ctx.attr.size; if pos > ctx.attr.size { // if we write pass file size set the new size @@ -2462,6 +2497,13 @@ impl EncryptedFs { return ino; } } + /// Enables the IPFS plugin using the provided master key. + /// Fixes Point 1 and removes Undefined Behavior by using a safe RwLock synchronization primitive. + pub fn enable_ipfs_plugin(&self, master_key: Vec) { + if let Ok(mut guard) = self.ipfs_cipher.write() { + *guard = Some(crate::ipfs_plugin::IpfsCipher::new(master_key)); + } + } } pub struct CopyFileRangeReq { src_ino: u64, diff --git a/src/lib.rs b/src/lib.rs index 22657c91..e29d075f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -326,3 +326,6 @@ pub const fn is_debug() -> bool { } false } + +#[path = "../ipfs_plugin/src/lib.rs"] +pub mod ipfs_plugin;