Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6abd836
feat: adaugat structura de baza pentru ipfs_plugin
razvanlazar2108 Jul 8, 2026
29c1c5e
chore: adaugat importurile necesare pentru criptare si manipulare str…
razvanlazar2108 Jul 8, 2026
cb3572e
feat: definit structura de date IpfsCipher cu campurile pentru cheie …
razvanlazar2108 Jul 8, 2026
f2bd2b5
feat: adaugat semnatura functiei de initializare pentru IpfsCipher
razvanlazar2108 Jul 8, 2026
12db078
feat: implementat instantierea corecta a cheii secrete si setarea cif…
razvanlazar2108 Jul 8, 2026
b3a0ae8
feat: definit semnatura encrypt si adaugat tratarea cazului pentru da…
razvanlazar2108 Jul 8, 2026
8f2ac30
feat: initializat Cursor in memorie si instantiat RingCryptoWrite
razvanlazar2108 Jul 8, 2026
bfa24f2
feat: finalizat procesul de scriere criptata si extragerea blocului d…
razvanlazar2108 Jul 8, 2026
a68daaf
feat: definit semnatura decrypt si validarea initiala a buffer-ului
razvanlazar2108 Jul 8, 2026
7c130ee
feat: finalizat logica de decriptare nativa utilizand RingCryptoRead
razvanlazar2108 Jul 8, 2026
f6f579b
test: configurat modulul de teste unitare izolate pentru plugin
razvanlazar2108 Jul 8, 2026
eeb8772
test: initializat datele mock si cheia de 32 de bytes
razvanlazar2108 Jul 8, 2026
02f0364
test: adaugat verificarile finale pentru a valida fluxul complet
razvanlazar2108 Jul 8, 2026
387034e
feat: integrat IpfsCipher ca camp optional in structura principala En…
razvanlazar2108 Jul 8, 2026
23d95a0
chore: mutat ipfs_plugin in folder dedicat in root conform cerintelor…
razvanlazar2108 Jul 8, 2026
1a0fb47
chore: fix code formatting using cargo fmt
razvanlazar2108 Jul 10, 2026
5ba0476
chore: translate comments, errors, and tests to English
razvanlazar2108 Jul 10, 2026
9fea777
feat: add key length validation for ChaCha20Poly1305 and add unit test
razvanlazar2108 Jul 10, 2026
59a632e
refactor: use standard std::io::Result and enrich error context
razvanlazar2108 Jul 10, 2026
aa3a2f7
refactor: change enable_ipfs_plugin to &self to support Arc ownership…
razvanlazar2108 Jul 10, 2026
06580f3
perf: reuse decryption buffer and optimize encryption capacity alloca…
razvanlazar2108 Jul 10, 2026
f7e7842
refactor: replace unsafe pointer casting with safe RwLock synchroniza…
razvanlazar2108 Jul 11, 2026
7dfa5b3
feat(ipfs): integrate plugin hook into filesystem write path
razvanlazar2108 Jul 11, 2026
a165ef8
feat(ipfs): integrate plugin hook into filesystem read path
razvanlazar2108 Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions ipfs_plugin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
cipher: Cipher,
dec_buffer: Mutex<Vec<u8>>, // Reusable buffer for decryption
}

impl IpfsCipher {
/// Initializes the plugin with a secure key and the default cipher
pub fn new(secret_key: Vec<u8>) -> Self {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No key-length validation — a wrong-length key panics (and aborts the process in release builds) despite the Result signature.

new() accepts any Vec<u8>. The first encrypt()/decrypt() then reaches UnboundKey::new(algorithm, &key.expose_secret()).expect("unbound key") (src/crypto/write.rs:92, and the .unwrap() at read.rs:93); ring returns Err for any key that isn't exactly Cipher::key_len() bytes (32 here), so the .expect fires. With [profile.release] panic = "abort", that's a full process abort — from a safe public API whose methods promise Result<_, String>. Every existing key path avoids this because crypto::derive_key sizes the key to cipher.key_len(); this constructor is the only way to smuggle in a bad length.

Validate in new() (return Result and check secret_key.len() == self.cipher.key_len()), or better, accept SecretVec<u8> from a source that already guarantees the length.


Generated by Claude Code

// 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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding ChaCha20Poly1305 ignores the cipher the filesystem was configured with.

EncryptedFs supports Aes256Gcm too and stores its configured cipher in self.cipher, but enable_ipfs_plugin doesn't pass it through — so on an AES-256-GCM filesystem, IPFS-bound data would be encrypted with a different cipher than the rest of the store, using the same master key across two algorithms. Every existing crypto entry point (create_write, create_read, crypto::encrypt/decrypt, derive_key) takes cipher: Cipher as a parameter; new() should as well, with the fs passing self.cipher.


Generated by Claude Code

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<Vec<u8>> {
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every encrypt() call pays ~512 KB of zeroed allocations, two ring key expansions, and an OS-entropy RNG construction — most of it dead weight.

Because Cursor<Vec<u8>> implements Write + Seek + Read, RingCryptoWrite::new takes the seek-capable branch (writer.as_write_seek_read().is_some(), src/crypto/write.rs:98) even though this is a plain sequential write: it builds a second UnboundKey, an OpeningKey, and a zeroed ~256 KB decrypt_buf (write.rs:100-104) that are never used, on top of the ~256 KB write buffer (write.rs:96) and a fresh ChaCha20Rng::from_entropy() (write.rs:93 → crypto.rs:347). decrypt() likewise re-expands the key and zeroes a ~256 KB scratch buffer per call (read.rs:91-95). For the intended use — encrypting many small IPFS chunks — the setup cost dwarfs the actual AEAD work.

Cheaper: cache reusable cipher state in IpfsCipher instead of rebuilding it per call, and give the output Vecs capacity hints (ciphertext size is computable: data.len() + 28 * ceil(len/256KiB); here Cursor::new(Vec::new()) and the Vec::new() before read_to_end at line 48 both start at zero capacity).


Generated by Claude Code


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<Vec<u8>> {
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);
}
}
42 changes: 42 additions & 0 deletions src/encryptedfs.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -574,6 +575,7 @@ pub struct EncryptedFs {
sizes_read: Mutex<HashMap<u64, AtomicU64>>,
requested_read: Mutex<HashMap<u64, AtomicU64>>,
read_only: bool,
pub ipfs_cipher: std::sync::RwLock<Option<IpfsCipher>>,
}

impl EncryptedFs {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<u8>) {
if let Ok(mut guard) = self.ipfs_cipher.write() {
*guard = Some(crate::ipfs_plugin::IpfsCipher::new(master_key));
}
}
}
pub struct CopyFileRangeReq {
src_ino: u64,
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,6 @@ pub const fn is_debug() -> bool {
}
false
}

#[path = "../ipfs_plugin/src/lib.rs"]
pub mod ipfs_plugin;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #[path] graft gives ipfs_plugin/ the layout of a standalone crate while actually compiling it unconditionally into the core library — the isolation of neither.

ipfs_plugin/ contains only src/lib.rs (no Cargo.toml), and its use crate::crypto::... imports only resolve when grafted in here. So the directory looks like a workspace crate (inviting someone to add it to [workspace] members, which would fail), is invisible to per-package tooling, and yet ships as rencfs::ipfs_plugin — unconditional, un-feature-gated, semver-public API in every build.

The repo already has the right pattern for an integration bridge: java-bridge/ is a real separate crate with its own Cargo.toml that depends on rencfs as a library. Either do that (which also lets the plugin gain actual IPFS dependencies later without touching the core crate), or make it an honest module at src/ipfs_plugin.rs, ideally behind a #[cfg(feature = "ipfs")] gate. Worth noting: nothing here is IPFS-specific yet — no CIDs, chunking, or networking — it's a generic buffer-crypto wrapper with an aspirational name.


Generated by Claude Code