diff --git a/src/crypto.rs b/src/crypto.rs index 9393daa..e4e06ef 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -104,7 +104,20 @@ pub fn check_password_prefix_collision(pass0: &str, pass1: &str) -> Result<()> { } /// Leitet aus dem Master-Passwort und dem Salt einen 256-Bit Key Encryption Key (KEK) via Argon2id ab. -pub fn derive_kek(password: &str, salt: &[u8], params: &KdfParams) -> Result> { +/// RAII-Guard für kurzzeitige Stack-Puffer, um Lock-Leaks bei Fehlern oder Rückgabe zu verhindern (V-03). +struct ScopedMemoryLock(*const u8, usize); + +impl Drop for ScopedMemoryLock { + fn drop(&mut self) { + crate::windows::unlock_memory(self.0, self.1); + } +} + +pub fn derive_kek( + password: &str, + salt: &[u8; 16], + params: &KdfParams, +) -> Result> { validate_kdf_params(params)?; let argon2_params = Params::new( @@ -118,12 +131,11 @@ pub fn derive_kek(password: &str, salt: &[u8], params: &KdfParams) -> Result Result Zeroizing<[u8; 32]> { let mut dek = Zeroizing::new([0u8; 32]); let _ = crate::windows::lock_memory(dek.as_ptr(), 32); + let _lock_guard = ScopedMemoryLock(dek.as_ptr(), 32); OsRng.fill_bytes(&mut *dek); dek } diff --git a/src/vfs.rs b/src/vfs.rs index 4478e0f..8bdc73d 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -94,6 +94,42 @@ impl DavDirEntry for SanctumDirEntry { } } +/// RAII-Guard für im physischen RAM verriegelte Schlüssel (VirtualLock / mlock). +/// Entriegelt den Speicherbereich via VirtualUnlock / munlock erst beim Drop der letzten verbleibenden Referenz (V-03). +#[derive(Debug)] +pub struct MemoryLockGuard(Zeroizing<[u8; 32]>); + +impl MemoryLockGuard { + pub fn new(key: Zeroizing<[u8; 32]>) -> Self { + crate::windows::lock_memory(key.as_ptr(), 32); + Self(key) + } + + pub fn key(&self) -> &Zeroizing<[u8; 32]> { + &self.0 + } +} + +impl std::ops::Deref for MemoryLockGuard { + type Target = Zeroizing<[u8; 32]>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8; 32]> for MemoryLockGuard { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl Drop for MemoryLockGuard { + fn drop(&mut self) { + crate::windows::unlock_memory(self.0.as_ptr(), 32); + } +} + // --------------------------------------------------------------------------- // Datei-Handle mit Streaming & Chunk-Pufferung // --------------------------------------------------------------------------- @@ -103,7 +139,7 @@ pub struct SanctumFile { file_size: u64, cursor: u64, db: Database, - dek: Arc>, + dek: Arc, meta: SanctumMetaData, // (chunk_index, decrypted_payload, is_dirty) cached_chunk: Option<(u32, Vec, bool)>, @@ -126,7 +162,7 @@ impl SanctumFile { pub fn new( node: NodeRecord, db: Database, - dek: Arc>, + dek: Arc, format_version: u32, last_activity: Arc, append: bool, @@ -445,9 +481,9 @@ impl DavFile for SanctumFile { #[derive(Clone)] pub struct SanctumFs { db: Database, - dek: Arc>, + dek: Arc, #[allow(dead_code)] - carrier_dek: Option>>, + carrier_dek: Option>, carrier_node_id: Option, carrier_fs: Option, format_version: u32, @@ -493,10 +529,10 @@ impl SanctumFs { .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); - let dek_arc = Arc::new(dek); - let carrier_dek_arc = carrier_dek.map(Arc::new); + let dek_guard = Arc::new(MemoryLockGuard::new(dek)); + let carrier_dek_guard = carrier_dek.map(|k| Arc::new(MemoryLockGuard::new(k))); - let db = db.with_session(vault_id, (*dek_arc).clone()); + let db = db.with_session(vault_id, (*dek_guard.key()).clone()); // Im Decoy-Vault (Slot 0): Stelle sicher, dass carrier_node_id stets bekannt ist, // um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen. @@ -509,12 +545,12 @@ impl SanctumFs { }); let carrier_fs = if vault_id == 1 { - if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_arc, carrier_node_id) { + if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_guard, carrier_node_id) { match CarrierFs::load( db.clone(), c_nid, - c_dek.clone(), - dek_arc.clone(), + Arc::new((*c_dek.key()).clone()), + Arc::new((*dek_guard.key()).clone()), format_version, anti_leak, ) { @@ -531,16 +567,10 @@ impl SanctumFs { None }; - // Forensischer RAM-Paging-Schutz via VirtualLock (verhindert Auslagerung in pagefile.sys) - crate::windows::lock_memory(dek_arc.as_ptr(), 32); - if let Some(ref c_dek) = carrier_dek_arc { - crate::windows::lock_memory(c_dek.as_ptr(), 32); - } - Self { db, - dek: dek_arc, - carrier_dek: carrier_dek_arc, + dek: dek_guard, + carrier_dek: carrier_dek_guard, carrier_node_id, carrier_fs, format_version, @@ -550,6 +580,10 @@ impl SanctumFs { } } + pub fn dek_strong_count(&self) -> usize { + Arc::strong_count(&self.dek) + } + pub fn vault_id(&self) -> u32 { self.vault_id } @@ -636,15 +670,6 @@ pub fn validate_path_safety(path: &str) -> Result<(), FsError> { Ok(()) } -impl Drop for SanctumFs { - fn drop(&mut self) { - crate::windows::unlock_memory(self.dek.as_ptr(), 32); - if let Some(ref c_dek) = self.carrier_dek { - crate::windows::unlock_memory(c_dek.as_ptr(), 32); - } - } -} - impl DavFileSystem for SanctumFs { fn open<'a>( &'a self, diff --git a/tests/mount_security_test.rs b/tests/mount_security_test.rs index 5b83145..a0fe39f 100644 --- a/tests/mount_security_test.rs +++ b/tests/mount_security_test.rs @@ -154,3 +154,68 @@ fn test_mount_security_multi_auth_and_no_token_in_url() { "Saubere URI darf kein Token enthalten" ); } + +#[tokio::test] +async fn test_vfs_memory_lock_retention_on_clone_v03() { + use dav_server::davpath::DavPath; + use dav_server::fs::{DavFileSystem, ReadDirMeta}; + use futures_util::StreamExt; + use sanctum::vfs::SanctumFs; + + let temp_dir = std::env::temp_dir(); + let container_path: PathBuf = + temp_dir.join(format!("test_v03_lock_{}.sanctum", std::process::id())); + if container_path.exists() { + let _ = std::fs::remove_file(&container_path); + } + + let salt = generate_salt(); + let kdf_params = KdfParams { + memory_cost: MIN_MEMORY_COST_KIB, + time_cost: MIN_TIME_COST, + parallelism: 1, + }; + let kek = derive_kek("TestPassV03!", &salt, &kdf_params).unwrap(); + let dek = generate_dek(); + let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap(); + + let db = Database::open(&container_path).unwrap(); + db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag) + .unwrap(); + + // 1. Initialisiere SanctumFs + let fs = SanctumFs::new(db.clone(), dek, sanctum::crypto::FORMAT_VERSION); + assert_eq!( + fs.dek_strong_count(), + 1, + "Anfangs muss genau 1 Referenz auf den verriegelten DEK existieren" + ); + + // 2. Klone SanctumFs (wie bei jeder HTTP/WebDAV-Anfrage im Server) + let fs_clone = fs.clone(); + assert_eq!( + fs.dek_strong_count(), + 2, + "Nach dem Klonen müssen 2 Referenzen existieren" + ); + + // 3. Droppe den Klon + drop(fs_clone); + + // 4. V-03: Der Refcount muss nun wieder 1 sein. Der Speicherbereich darf NICHT + // vorzeitig über Drop eines Klons entriegelt worden sein! + assert_eq!( + fs.dek_strong_count(), + 1, + "Nach Drop des Klons muss genau 1 Referenz erhalten bleiben" + ); + + // Verifiziere funktionale Nutzbarkeit nach Drop des Klons + let root_path = DavPath::new("/").unwrap(); + let mut entries = fs.read_dir(&root_path, ReadDirMeta::None).await.unwrap(); + assert!(entries.next().await.is_none()); + + drop(fs); + let _ = std::fs::remove_file(&container_path); +} +