Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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.

77 changes: 77 additions & 0 deletions ipfs_plugin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use crate::crypto::{self, Cipher};
use crate::crypto::write::CryptoWrite; // Rezolvă eroarea cu .finish()
use shush_rs::SecretVec;
use std::io::{Read, Write, Cursor};

pub struct IpfsCipher {
key: SecretVec<u8>,
cipher: Cipher,
}

impl IpfsCipher {
/// Inițializează plugin-ul cu o cheie sigură și cipher-ul implicit
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

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

}
}

/// Criptează un bloc de date utilizând un Cursor pentru a simula un fișier în memorie
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, String> {

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.

This re-implements the one-shot pattern that already exists in src/crypto.rs, with a worse error type.

crypto::encrypt/crypto::decrypt (src/crypto.rs:197-216) do exactly this — Cursor::new(vec[])create_writewrite_allfinishinto_inner, and create_readread_to_*. A byte-slice variant belongs next to them (e.g. encrypt_bytes/decrypt_bytes returning crypto::Result<Vec<u8>>) so future fixes to the finish/flush or nonce handling propagate to all callers instead of leaving a divergent copy here.

Related: Result<Vec<u8>, String> with format!("...: {:?}", e) discards the crate's error structure — an AEAD tag mismatch (integrity failure) becomes indistinguishable from a plain IO error, and callers can't use ?. Both underlying operations return io::Error, which crypto::Error already converts via #[from]. (The data.is_empty() early-returns are also redundant — the streams already produce/accept empty input, covered by test_encrypt_decrypt_empty_string.)


Generated by Claude Code

if data.is_empty() {
return Ok(Vec::new());
}

// Folosim Cursor pentru a implementa Write + Seek + Read cerute de autor
let memory_file = Cursor::new(Vec::new());
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| format!("Eroare la scrierea datelor IPFS: {:?}", e))?;

// .finish() returnează obiectul intern (Cursor-ul) în caz de succes
let finished_cursor = writer.finish()
.map_err(|e| format!("Eroare la finalizarea criptării IPFS: {:?}", e))?;

// Extriem vectorul de bytes din interiorul cursorului
Ok(finished_cursor.into_inner())
}

/// Decriptează un bloc de date utilizând CryptoRead-ul nativ din rencfs
pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>, String> {
if encrypted_data.is_empty() {
return Ok(Vec::new());
}

let mut reader = crypto::create_read(encrypted_data, self.cipher, &self.key);
let mut decrypted_data = Vec::new();

reader.read_to_end(&mut decrypted_data)
.map_err(|e| format!("Eroare la decriptarea datelor IPFS: {:?}", e))?;

Ok(decrypted_data)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_ipfs_encryption_decryption() {
// Generăm o cheie de test de 32 de bytes (pentru ChaCha20Poly1305)
let test_key = vec![0u8; 32];
let cipher = IpfsCipher::new(test_key);

let original_data = b"Date secrete trimise prin IPFS cu CID unic!";

// 1. Criptăm datele
let encrypted = cipher.encrypt(original_data).expect("Criptarea a eșuat");
assert_ne!(original_data.to_vec(), encrypted, "Datele criptate nu trebuie să fie la fel ca cele originale");

// 2. Decriptăm datele înapoi
let decrypted = cipher.decrypt(&encrypted).expect("Decriptarea a eșuat");
assert_eq!(original_data.to_vec(), decrypted, "Datele decriptate nu se potrivesc cu cele originale");
}
}
8 changes: 8 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: Option<IpfsCipher>,

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.

Dead public field on the security-critical core type.

ipfs_cipher is never read by any code in the tree — no FUSE op, read/write path, CLI flag, or test consults it — and it's the only pub field on EncryptedFs (everything else, including the key, is private by design). Consequences:

  • Being pub, external code with &mut access could replace it wholesale with an IpfsCipher built from an arbitrary key, bypassing all encapsulation.
  • It permanently commits an experimental, unused field to the crate's semver-public API surface.
  • Since IpfsCipher only wraps the already-public crypto::create_write/create_read, the core struct doesn't need to know about it at all — the IPFS integration can hold its own cipher and consume the public crypto API from outside.

Suggest deleting the field (and enable_ipfs_plugin) until something actually consumes it.


Generated by Claude Code

}

impl EncryptedFs {
Expand Down Expand Up @@ -627,6 +629,7 @@ impl EncryptedFs {
sizes_read: Mutex::default(),
requested_read: Mutex::default(),
read_only,
ipfs_cipher: None,
};

let arc = Arc::new(fs);
Expand All @@ -638,6 +641,8 @@ impl EncryptedFs {
arc.ensure_root_exists().await?;

Ok(arc)


}
Comment on lines 643 to 644

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.

cargo fmt --all -- --check fails on this PR — these two stray blank lines (one contains only trailing whitespace) plus the missing trailing newline and unformatted lines in ipfs_plugin/src/lib.rs all trip rustfmt. Verified by running the check on this branch: it reports diffs in src/encryptedfs.rs and 6 locations in ipfs_plugin/src/lib.rs. That means ./scripts/check-before-push.sh (mandated by CLAUDE.md before committing) and the CI formatting gate cannot pass as-is. Please run cargo fmt --all.

Suggested change
Ok(arc)
}
Ok(arc)
}

Generated by Claude Code


pub fn exists(&self, ino: u64) -> bool {
Expand Down Expand Up @@ -2462,6 +2467,9 @@ impl EncryptedFs {
return ino;
}
}
pub fn enable_ipfs_plugin(&mut self, master_key: Vec<u8>) {

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.

This method is uncallable — the feature is dead on arrival.

enable_ipfs_plugin takes &mut self, but the only constructor, EncryptedFs::new, returns FsResult<Arc<Self>> (and stores a self_weak clone internally, so Arc::get_mut fails too). Every consumer — the FUSE mount path, java-bridge, the rustdoc examples — holds Arc<EncryptedFs>, which never yields &mut. Any attempt to call this compiles to "cannot borrow data in an Arc as mutable", so ipfs_cipher can only ever remain None; nothing in this PR (including its test) calls it.

If core support is truly needed, this has to fit the type's ownership model: a constructor parameter, or an interior-mutability slot — though see the other comments for why the plugin probably shouldn't touch EncryptedFs at all.


Generated by Claude Code

self.ipfs_cipher = Some(IpfsCipher::new(master_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.

Taking the master key as a plain Vec<u8> — and keeping it for the lifetime of the filesystem — breaks the crate's documented key-handling model.

CLAUDE.md states secrets are "held in shush_rs types (mlock/mprotect/zeroize) and dropped after inactivity", and docs/claude/encryptedfs.md documents the master key living only in ExpireValue<SecretVec<u8>> with a 10-minute expiry. This API forces the opposite on both counts:

  • There is no accessor that yields the key as Vec<u8>, so a caller would have to do key.get().await.expose_secret().to_vec() — materializing an unprotected, non-zeroized heap copy (swappable, visible in core dumps) just to cross this boundary.
  • The resulting IpfsCipher then holds the key with no expiry for as long as the fs lives, outside the ExpireValue mechanism.
  • It also reuses the raw master key for a second cryptographic purpose with no domain separation — a leak or break in the IPFS context compromises the entire data dir.

If the plugin needs a key, accept &SecretVec<u8> end-to-end (like every function in src/crypto.rs) and preferably derive a purpose-specific subkey from the master key rather than handing out the master key itself.


Generated by Claude Code

}
}
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