use std::io::SeekFrom; use std::path::PathBuf; use bytes::Bytes; use dav_server::{ davpath::DavPath, fs::{DavFileSystem, OpenOptions, ReadDirMeta}, }; use futures_util::StreamExt; use sanctum::{ crypto::{ dek_to_mnemonic, derive_kek, encrypt_chunk, generate_dek, generate_salt, unwrap_dek, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1, }, recovery::{export_header_backup, restore_header_backup, restore_header_from_recovery_key}, storage::Database, verify::verify_container, vfs::SanctumFs, }; #[tokio::test] async fn test_sanctum_full_container_lifecycle() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_sanctum_{}.sanctum", std::process::id())); // Aufräumen, falls alte Testdatei existiert if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password = "CorrectMasterPassword2026!"; let wrong_password = "WrongMasterPassword!"; // 1. Initialisierung let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, // Schnell für Tests time_cost: 1, parallelism: 1, }; let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation"); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("DEK wrapping"); let db = Database::open(&container_path).expect("Open database"); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag) .expect("Init schema"); db.checkpoint().expect("Checkpoint"); // 2. Header & Magic Bytes Prüfung let meta = db.read_meta().expect("Read meta"); assert_eq!(meta.version, FORMAT_VERSION); assert_eq!(meta.kdf_salt, salt); assert_eq!(meta.wrapped_dek, wrapped_dek); // Falsches Passwort schlägt fehl let wrong_kek = derive_kek(wrong_password, &meta.kdf_salt, &meta.kdf_params).unwrap(); assert!(unwrap_dek(&wrong_kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag).is_err()); // Richtiges Passwort entschlüsselt DEK let unwrapped_dek = unwrap_dek(&kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag).unwrap(); assert_eq!(*dek, *unwrapped_dek); // 3. VFS Filesystem-Operationen (WebDAV Trait) let fs = SanctumFs::new(db.clone(), unwrapped_dek, meta.version); // Ordner erstellen let docs_path = DavPath::new("/documents").unwrap(); fs.create_dir(&docs_path).await.expect("Create /documents"); let docs_meta = fs.metadata(&docs_path).await.expect("Metadata /documents"); assert!(docs_meta.is_dir()); // Multi-MB Datei über mehrere Chunks hinweg schreiben (2.5 MB = 3 Chunks à 1 MB) let file_path = DavPath::new("/documents/large_payload.bin").unwrap(); let mut opts_write = OpenOptions::default(); opts_write.write = true; opts_write.create_new = true; let mut file = fs.open(&file_path, opts_write).await.expect("Open for write"); let payload_size = (2.5 * CHUNK_SIZE as f64) as usize; let mut sample_data = Vec::with_capacity(payload_size); for i in 0..payload_size { sample_data.push((i % 251) as u8); } file.write_bytes(Bytes::copy_from_slice(&sample_data)) .await .expect("Write 2.5 MB"); file.flush().await.expect("Flush file"); drop(file); // Metadaten verifizieren let file_meta = fs.metadata(&file_path).await.expect("Metadata file"); assert_eq!(file_meta.len(), payload_size as u64); assert!(!file_meta.is_dir()); // Datei lesen & Seek über Chunk-Grenzen testen let mut opts_read = OpenOptions::default(); opts_read.read = true; let mut read_file = fs.open(&file_path, opts_read).await.expect("Open for read"); // Seek mitten in den 2. Chunk (1 MB + 500 Bytes) let seek_offset = (CHUNK_SIZE + 500) as u64; let new_pos = read_file .seek(SeekFrom::Start(seek_offset)) .await .expect("Seek"); assert_eq!(new_pos, seek_offset); // 2000 Bytes lesen (überschreitet evtl. Chunk-Grenze oder bleibt im Chunk) let read_len = 2000; let read_chunk_bytes = read_file.read_bytes(read_len).await.expect("Read bytes"); assert_eq!(read_chunk_bytes.len(), read_len); assert_eq!( &read_chunk_bytes[..], &sample_data[seek_offset as usize..seek_offset as usize + read_len] ); drop(read_file); // Datei umbenennen let renamed_path = DavPath::new("/documents/renamed_payload.bin").unwrap(); fs.rename(&file_path, &renamed_path).await.expect("Rename file"); assert!(fs.metadata(&file_path).await.is_err()); let renamed_meta = fs.metadata(&renamed_path).await.expect("Metadata renamed"); assert_eq!(renamed_meta.len(), payload_size as u64); // Datei kopieren let copy_path = DavPath::new("/documents/copy_payload.bin").unwrap(); fs.copy(&renamed_path, ©_path).await.expect("Copy file"); let copy_meta = fs.metadata(©_path).await.expect("Metadata copy"); assert_eq!(copy_meta.len(), payload_size as u64); // Verzeichnis auflisten let stream = fs.read_dir(&docs_path, ReadDirMeta::None).await.expect("Read dir"); let entries: Vec<_> = stream.collect().await; assert_eq!(entries.len(), 2); // renamed_payload.bin & copy_payload.bin // Dateien und Verzeichnis löschen fs.remove_file(&renamed_path).await.expect("Remove renamed"); fs.remove_file(©_path).await.expect("Remove copy"); fs.remove_dir(&docs_path).await.expect("Remove dir"); // Prüfen, dass Root leer ist let root_path = DavPath::new("/").unwrap(); let root_stream = fs.read_dir(&root_path, ReadDirMeta::None).await.expect("Read root"); let root_entries: Vec<_> = root_stream.collect().await; assert_eq!(root_entries.len(), 0); // Finaler DB Checkpoint db.checkpoint().expect("Final checkpoint"); drop(fs); drop(db); // Verifizieren, dass die Containerdatei existiert und aufgeräumt werden kann assert!(container_path.exists()); let _ = std::fs::remove_file(&container_path); } #[tokio::test] async fn test_sanctum_password_change() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_sanctum_passwd_{}.sanctum", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password_v1 = "InitialSecret123!"; let password_v2 = "NewSecret456!"; // 1. Initialisierung mit Passwort v1 let salt_v1 = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek_v1 = derive_kek(password_v1, &salt_v1, &kdf_params).expect("KEK v1"); let original_dek = generate_dek(); let (wrapped_dek_v1, nonce_v1, tag_v1) = wrap_dek(&kek_v1, &original_dek).expect("Wrap DEK v1"); let db = Database::open(&container_path).expect("Open database"); db.init_schema(&salt_v1, &kdf_params, &wrapped_dek_v1, &nonce_v1, &tag_v1) .expect("Init schema"); db.checkpoint().expect("Checkpoint"); // Datei im Container mit DEK anlegen let fs_v1 = SanctumFs::new(db.clone(), original_dek.clone(), FORMAT_VERSION); let file_path = DavPath::new("/secret.txt").unwrap(); let file_data = b"Verschluesselte Geheimdaten vor der Passwortaenderung!"; let mut opts_write = OpenOptions::default(); opts_write.write = true; opts_write.create_new = true; let mut write_file = fs_v1 .open(&file_path, opts_write) .await .expect("Open write"); write_file .write_bytes(Bytes::from_static(file_data)) .await .expect("Write bytes"); write_file.flush().await.expect("Flush"); drop(write_file); drop(fs_v1); db.checkpoint().expect("Checkpoint"); // 2. Passwortwechsel durchführen let meta_before = db.read_meta().expect("Read meta"); let old_kek = derive_kek(password_v1, &meta_before.kdf_salt, &meta_before.kdf_params).expect("Derive old KEK"); let recovered_dek = unwrap_dek(&old_kek, &meta_before.wrapped_dek, &meta_before.header_nonce, &meta_before.header_tag) .expect("Unwrap with old password"); let salt_v2 = generate_salt(); let kek_v2 = derive_kek(password_v2, &salt_v2, &kdf_params).expect("Derive new KEK"); let (wrapped_dek_v2, nonce_v2, tag_v2) = wrap_dek(&kek_v2, &recovered_dek).expect("Wrap with new KEK"); db.update_meta_keys(&salt_v2, &kdf_params, &wrapped_dek_v2, &nonce_v2, &tag_v2).expect("Update meta keys"); db.checkpoint().expect("Checkpoint"); // 3. Verifikation: Altes Passwort darf NICHT mehr funktionieren let meta_after = db.read_meta().expect("Read meta after"); let old_kek_again = derive_kek(password_v1, &meta_after.kdf_salt, &meta_after.kdf_params).unwrap(); assert!( unwrap_dek(&old_kek_again, &meta_after.wrapped_dek, &meta_after.header_nonce, &meta_after.header_tag).is_err(), "Altes Passwort darf nach Passwortaenderung nicht mehr funktionieren!" ); // 4. Verifikation: Neues Passwort funktioniert und entschlüsselt alte Daten intakt let new_kek = derive_kek(password_v2, &meta_after.kdf_salt, &meta_after.kdf_params).expect("Derive new KEK"); let active_dek = unwrap_dek(&new_kek, &meta_after.wrapped_dek, &meta_after.header_nonce, &meta_after.header_tag) .expect("Unwrap with new password"); let fs_v2 = SanctumFs::new(db.clone(), active_dek, meta_after.version); let mut opts_read = OpenOptions::default(); opts_read.read = true; let mut read_file = fs_v2.open(&file_path, opts_read).await.expect("Open read with new password"); let read_bytes = read_file.read_bytes(file_data.len()).await.expect("Read bytes"); assert_eq!(&read_bytes[..], file_data, "Daten muessen nach Passwortaenderung unveraendert lesbar sein!"); drop(read_file); drop(fs_v2); drop(db); let _ = std::fs::remove_file(&container_path); } #[tokio::test] async fn test_sanctum_lz4_compression_efficiency() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_sanctum_compress_{}.sanctum", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password = "CompressTestPassword2026!"; let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek = derive_kek(password, &salt, &kdf_params).expect("KEK"); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap"); let db = Database::open(&container_path).expect("open db"); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).expect("init schema"); let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION); // 2 MB hochkompressible Textdaten erzeugen (z. B. wiederholende Zeilen) let pattern = b"Sanctum high performance encrypted container storage with transparent LZ4 compression.\n"; let target_size = 2 * 1024 * 1024; let mut repetitive_data = Vec::with_capacity(target_size); while repetitive_data.len() < target_size { repetitive_data.extend_from_slice(pattern); } repetitive_data.truncate(target_size); // Datei über VFS schreiben let test_file_path = DavPath::new("/compressed_test.log").unwrap(); let mut opts = OpenOptions::default(); opts.write = true; opts.create_new = true; let mut file = fs.open(&test_file_path, opts).await.expect("open write"); file.write_bytes(Bytes::copy_from_slice(&repetitive_data)).await.expect("write"); file.flush().await.expect("flush"); drop(file); db.checkpoint().expect("checkpoint"); // Chunk-Knoten ermitteln und Ciphertext-Größe in der Datenbank prüfen let node = db.resolve_path("/compressed_test.log").expect("resolve").expect("found"); assert_eq!(node.size, target_size as u64); let chunk0 = db.read_chunk(node.id, 0).expect("read chunk").expect("chunk 0 exists"); // 1 MB Rohdaten komprimiert mit LZ4 sollte typischerweise < 100 KB sein assert!( chunk0.ciphertext.len() < 100_000, "Ciphertext should be compressed from 1MB to < 100KB, was {}", chunk0.ciphertext.len() ); // Datei zurücklesen und mit Original vergleichen let mut read_opts = OpenOptions::default(); read_opts.read = true; let mut read_file = fs.open(&test_file_path, read_opts).await.expect("open read"); let read_back = read_file.read_bytes(target_size).await.expect("read back"); assert_eq!(read_back.len(), target_size); assert_eq!(&read_back[..], &repetitive_data[..]); drop(read_file); drop(fs); drop(db); let _ = std::fs::remove_file(&container_path); } #[tokio::test] async fn test_sanctum_v1_backward_compatibility() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_sanctum_v1_compat_{}.sanctum", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password = "V1LegacyPassword!"; let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek = derive_kek(password, &salt, &kdf_params).expect("KEK"); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap"); let db = Database::open(&container_path).expect("open db"); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).expect("init schema"); // Formatversion auf V1 setzen db.set_meta_version(FORMAT_VERSION_V1).expect("set v1"); let meta = db.read_meta().expect("read meta"); assert_eq!(meta.version, FORMAT_VERSION_V1); // Datei-Knoten direkt in DB erstellen let node = db.create_node(1, "legacy_v1.txt", false).expect("create node"); // V1 Chunk mit encrypt_chunk(..., FORMAT_VERSION_V1) erzeugen und direkt in DB schreiben let v1_plaintext = b"Legacy Sanctum V1 uncompressed data payload."; let (ct, nonce, tag) = encrypt_chunk(&dek, node.id, 0, v1_plaintext, FORMAT_VERSION_V1).expect("encrypt v1"); db.write_chunk(node.id, 0, &nonce, &tag, &ct).expect("write chunk"); db.update_node_size_and_time(node.id, v1_plaintext.len() as u64, 12345678).expect("update size"); db.checkpoint().expect("checkpoint"); // Öffnen über SanctumFs konfiguriert für V1 let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION_V1); let path = DavPath::new("/legacy_v1.txt").unwrap(); let mut opts = OpenOptions::default(); opts.read = true; let mut file = fs.open(&path, opts).await.expect("open v1 file"); let read_data = file.read_bytes(v1_plaintext.len()).await.expect("read v1"); assert_eq!(&read_data[..], v1_plaintext); drop(file); drop(fs); drop(db); let _ = std::fs::remove_file(&container_path); } #[tokio::test] async fn test_sanctum_disaster_recovery_workflow() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_disaster_{}.sanctum", std::process::id())); let backup_path: PathBuf = temp_dir.join(format!("test_disaster_{}.sanctum.hdr", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } if backup_path.exists() { let _ = std::fs::remove_file(&backup_path); } let initial_password = "PrimaryPassword2026!"; let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek = derive_kek(initial_password, &salt, &kdf_params).expect("KEK"); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap"); // 1. Container erstellen und Datei schreiben let db = Database::open(&container_path).expect("open db"); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).expect("init schema"); let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION); let test_file = DavPath::new("/confidential.doc").unwrap(); let secret_payload = b"Top secret corporate documents protected by Sanctum."; let mut opts_w = OpenOptions::default(); opts_w.write = true; opts_w.create_new = true; let mut f = fs.open(&test_file, opts_w).await.expect("create file"); f.write_bytes(Bytes::from_static(secret_payload)).await.expect("write"); f.flush().await.expect("flush"); drop(f); drop(fs); db.checkpoint().expect("checkpoint"); // 2. Header-Backup exportieren & 24-Wort Notfallschlüssel sichern export_header_backup(&container_path, &backup_path).expect("Export header backup"); assert!(backup_path.exists()); let recovery_phrase = dek_to_mnemonic(&dek).expect("Generate 24-word recovery phrase"); assert_eq!(recovery_phrase.split_whitespace().count(), 24); // 3. Integritätsprüfung vor Katastrophe let initial_report = verify_container(&container_path, Some(&dek), true).expect("Verify initial"); assert!(initial_report.is_healthy()); assert_eq!(initial_report.total_files, 1); assert_eq!(initial_report.corrupted_chunks, 0); // 4. KATASTROPHE 1: Header in SQLite löschen let conn = rusqlite::Connection::open(&container_path).unwrap(); conn.execute("DELETE FROM meta", []).unwrap(); drop(conn); // Container darf sich ohne Header nicht mehr öffnen lassen let broken_db = Database::open(&container_path).unwrap(); assert!(broken_db.read_meta().is_err()); drop(broken_db); // 5. WIEDERHERSTELLUNG 1: Aus externem .sanctum.hdr Backup restoren restore_header_backup(&container_path, &backup_path).expect("Restore from backup file"); let restored_db = Database::open(&container_path).unwrap(); let restored_meta = restored_db.read_meta().expect("Read restored meta"); let restored_kek = derive_kek(initial_password, &restored_meta.kdf_salt, &restored_meta.kdf_params).unwrap(); let active_dek1 = unwrap_dek(&restored_kek, &restored_meta.wrapped_dek, &restored_meta.header_nonce, &restored_meta.header_tag).unwrap(); let fs1 = SanctumFs::new(restored_db.clone(), active_dek1, restored_meta.version); let mut opts_r = OpenOptions::default(); opts_r.read = true; let mut rf1 = fs1.open(&test_file, opts_r).await.expect("open restored"); let read_back1 = rf1.read_bytes(secret_payload.len()).await.expect("read"); assert_eq!(&read_back1[..], secret_payload, "Data must be intact after header restore"); drop(rf1); drop(fs1); drop(restored_db); // 6. KATASTROPHE 2: Header erneut zerstört UND ursprüngliches Passwort vergessen! let conn = rusqlite::Connection::open(&container_path).unwrap(); conn.execute("DELETE FROM meta", []).unwrap(); drop(conn); // 7. WIEDERHERSTELLUNG 2: Via 24-Wort Notfallschlüssel mit BRANDNEUEM Passwort let brand_new_password = "BrandNewRescuedVaultPassphrase2026!"; restore_header_from_recovery_key(&container_path, &recovery_phrase, brand_new_password) .expect("Restore from 24-word recovery key"); let rescued_db = Database::open(&container_path).unwrap(); let rescued_meta = rescued_db.read_meta().expect("Read rescued meta"); let rescued_kek = derive_kek(brand_new_password, &rescued_meta.kdf_salt, &rescued_meta.kdf_params).unwrap(); let active_dek2 = unwrap_dek(&rescued_kek, &rescued_meta.wrapped_dek, &rescued_meta.header_nonce, &rescued_meta.header_tag).unwrap(); let fs2 = SanctumFs::new(rescued_db.clone(), active_dek2, rescued_meta.version); let mut opts_r2 = OpenOptions::default(); opts_r2.read = true; let mut rf2 = fs2.open(&test_file, opts_r2).await.expect("open with new password"); let read_back2 = rf2.read_bytes(secret_payload.len()).await.expect("read"); assert_eq!(&read_back2[..], secret_payload, "Data must be intact after emergency recovery key rescue"); drop(rf2); drop(fs2); drop(rescued_db); // 8. BITROT-ERKENNUNG: Testen, dass verify korrumpierte Chunks detektiert let conn = rusqlite::Connection::open(&container_path).unwrap(); let mut current_ct: Vec = conn.query_row("SELECT ciphertext FROM chunks LIMIT 1", [], |r| r.get(0)).unwrap(); current_ct[0] ^= 0x01; // Bit-Flip conn.execute("UPDATE chunks SET ciphertext = ?1", rusqlite::params![current_ct]).unwrap(); drop(conn); let bitrot_report = verify_container(&container_path, Some(&dek), true).expect("Verify bitrot"); assert!(!bitrot_report.is_healthy(), "Container must flag bitrot"); assert_eq!(bitrot_report.corrupted_chunks, 1); // Aufräumen let _ = std::fs::remove_file(&container_path); let _ = std::fs::remove_file(&backup_path); } #[tokio::test] async fn test_anti_leak_and_inactivity_shield() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_leak_shield_{}.sanctum", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password = "ShieldedContainerPass2026!"; let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek = derive_kek(password, &salt, &kdf_params).expect("derive kek"); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap dek"); let db = Database::open(&container_path).expect("open db"); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag) .expect("init schema"); db.checkpoint().expect("checkpoint"); // 1. Erstelle Filesystem mit aktivem Anti-Leak Shield (Standard) let fs = SanctumFs::with_options(db.clone(), dek.clone(), FORMAT_VERSION, true); // 2. Windows Explorer Leck-Dateien müssen abgewehrt werden let leak_files = [ "/Thumbs.db", "/thumbs.db", "/desktop.ini", "/Desktop.ini", "/Folder.jpg", "/AlbumArt_{A1B2C3D4-E5F6}_Large.jpg", "/albumartsmall.jpg", "/ehthumbs.db", "/autorun.inf", ]; for leak in leak_files { let path = DavPath::new(leak).unwrap(); let mut opts = OpenOptions::default(); opts.write = true; opts.create_new = true; let res = fs.open(&path, opts).await; assert!( matches!(res, Err(dav_server::fs::FsError::Forbidden)), "Leak-Datei '{}' muss mit FsError::Forbidden blockiert werden", leak ); } // Ordnererstellung mit Leck-Namen muss ebenfalls blockiert werden let leak_dir = DavPath::new("/Thumbs.db").unwrap(); let dir_res = fs.create_dir(&leak_dir).await; assert!(matches!(dir_res, Err(dav_server::fs::FsError::Forbidden))); // 3. Legitime Dateien müssen reibungslos funktionieren let legit_path = DavPath::new("/top_secret.docx").unwrap(); let mut opts_legit = OpenOptions::default(); opts_legit.write = true; opts_legit.create_new = true; let mut file = fs.open(&legit_path, opts_legit).await.expect("create legit file"); file.write_bytes(Bytes::from_static(b"Sanctum OpSec Shield Test")) .await .expect("write data"); file.flush().await.expect("flush data"); drop(file); // 4. Inaktivitäts-Tracking überprüfen let activity_tracker = fs.last_activity(); let recorded_now = activity_tracker.load(std::sync::atomic::Ordering::Relaxed); assert!(recorded_now > 0); // Zurückdatieren um 300 Sekunden activity_tracker.store(recorded_now - 300, std::sync::atomic::Ordering::Relaxed); let past = activity_tracker.load(std::sync::atomic::Ordering::Relaxed); // Datei lesen let mut opts_read = OpenOptions::default(); opts_read.read = true; let mut rf = fs.open(&legit_path, opts_read).await.expect("open legit"); let content = rf.read_bytes(25).await.expect("read bytes"); assert_eq!(&content[..], b"Sanctum OpSec Shield Test"); drop(rf); let updated_time = activity_tracker.load(std::sync::atomic::Ordering::Relaxed); assert!( updated_time > past, "VFS-Aktivität muss den Inaktivitäts-Timer aktualisieren" ); // 5. Deaktivierter Shield (--no-anti-leak) erlaubt Explorer-Metadaten let unshielded_fs = SanctumFs::with_options(db.clone(), dek.clone(), FORMAT_VERSION, false); let mut unshielded_opts = OpenOptions::default(); unshielded_opts.write = true; unshielded_opts.create_new = true; let desktop_ini_path = DavPath::new("/desktop.ini").unwrap(); let desktop_res = unshielded_fs.open(&desktop_ini_path, unshielded_opts).await; assert!( desktop_res.is_ok(), "Ohne Shield muss Erstellung von desktop.ini gestattet sein" ); // Aufräumen let _ = std::fs::remove_file(&container_path); } #[tokio::test] async fn test_hidden_vault_and_storage_compaction_integration() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_hidden_{}.sanctum", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password_decoy = "DecoyOuterPassword2026!"; let password_hidden = "TopSecretHiddenPassword2026!"; // 1. Dual-Vault Initialisierung (Slot 0 = Decoy, Slot 1 = Hidden Vault) let salt0 = generate_salt(); let salt1 = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek0 = derive_kek(password_decoy, &salt0, &kdf_params).expect("KEK 0"); let kek1 = derive_kek(password_hidden, &salt1, &kdf_params).expect("KEK 1"); let dek0 = generate_dek(); let dek1 = generate_dek(); let (wrapped_dek0, nonce0, tag0) = wrap_dek(&kek0, &dek0).expect("Wrap DEK 0"); let (wrapped_dek1, nonce1, tag1) = wrap_dek(&kek1, &dek1).expect("Wrap DEK 1"); let db = Database::open(&container_path).expect("Open container"); db.init_schema_with_hidden( &salt0, &kdf_params, &wrapped_dek0, &nonce0, &tag0, Some((&salt1, &kdf_params, &wrapped_dek1, &nonce1, &tag1)), ) .expect("Init schema with hidden"); db.checkpoint().expect("Checkpoint init"); // 2. Multi-Slot Authentifizierungsprüfung let meta = db.read_meta().expect("Read meta"); assert_eq!(meta.slots.len(), 2, "Es müssen 2 Slots initialisiert sein"); // Decoy Passwort entsperrt Slot 0 let mut auth_decoy = None; for slot in &meta.slots { if let Ok(kek) = derive_kek(password_decoy, &slot.kdf_salt, &slot.kdf_params) { if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) { auth_decoy = Some((dek, slot.slot_id)); break; } } } let (unwrapped_dek0, slot_id0) = auth_decoy.expect("Decoy password must unwrap"); assert_eq!(slot_id0, 0); assert_eq!(*unwrapped_dek0, *dek0); // Hidden Passwort entsperrt Slot 1 let mut auth_hidden = None; for slot in &meta.slots { if let Ok(kek) = derive_kek(password_hidden, &slot.kdf_salt, &slot.kdf_params) { if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) { auth_hidden = Some((dek, slot.slot_id)); break; } } } let (unwrapped_dek1, slot_id1) = auth_hidden.expect("Hidden password must unwrap"); assert_eq!(slot_id1, 1); assert_eq!(*unwrapped_dek1, *dek1); // 3. VFS Operationen im Decoy-Vault (Slot 0) let fs_decoy = SanctumFs::with_vault(db.clone(), unwrapped_dek0, meta.version, true, 0); let decoy_file_path = DavPath::new("/harmless_recipe.txt").unwrap(); let mut opts_write = OpenOptions::default(); opts_write.write = true; opts_write.create_new = true; let mut df = fs_decoy.open(&decoy_file_path, opts_write).await.expect("Open decoy file"); df.write_bytes(Bytes::from_static(b"Apples, Flour, Sugar, Butter")) .await .expect("Write decoy"); df.flush().await.expect("Flush decoy"); drop(df); // 4. VFS Operationen im Hidden-Vault (Slot 1) let fs_hidden = SanctumFs::with_vault(db.clone(), unwrapped_dek1, meta.version, true, 1); let hidden_file_path = DavPath::new("/classified_report.pdf").unwrap(); let mut opts_write2 = OpenOptions::default(); opts_write2.write = true; opts_write2.create_new = true; let mut hf = fs_hidden.open(&hidden_file_path, opts_write2).await.expect("Open hidden file"); hf.write_bytes(Bytes::from_static(b"TOP SECRET INTELLIGENCE DATA")) .await .expect("Write hidden"); hf.flush().await.expect("Flush hidden"); drop(hf); // 5. Strikte Isolierung verifizieren: Decoy-Vault sieht NICHTS vom Hidden-Vault let mut stream_decoy = fs_decoy .read_dir(&DavPath::new("/").unwrap(), ReadDirMeta::None) .await .expect("read_dir decoy"); let mut decoy_entries = Vec::new(); while let Some(Ok(entry)) = stream_decoy.next().await { decoy_entries.push(String::from_utf8_lossy(&entry.name()).to_string()); } assert_eq!(decoy_entries, vec!["harmless_recipe.txt"]); assert!(fs_decoy.metadata(&hidden_file_path).await.is_err(), "Decoy darf classified_report nicht sehen"); // Hidden-Vault sieht ebenfalls nur seine eigenen Dateien let mut stream_hidden = fs_hidden .read_dir(&DavPath::new("/").unwrap(), ReadDirMeta::None) .await .expect("read_dir hidden"); let mut hidden_entries = Vec::new(); while let Some(Ok(entry)) = stream_hidden.next().await { hidden_entries.push(String::from_utf8_lossy(&entry.name()).to_string()); } assert_eq!(hidden_entries, vec!["classified_report.pdf"]); assert!(fs_hidden.metadata(&decoy_file_path).await.is_err(), "Hidden darf harmless_recipe nicht auflösen"); // 6. Forensische Dateiprüfung (Anti-Forensics / Plausible Deniability) db.checkpoint().expect("Checkpoint before raw inspection"); let raw_bytes = std::fs::read(&container_path).expect("Read container raw bytes"); // Der Dateiname "classified_report.pdf" darf NIRGENDS im Rohformat im Container stehen let needle_filename = b"classified_report"; assert!( !raw_bytes.windows(needle_filename.len()).any(|w| w == needle_filename), "Forensischer Leak: Dateiname des Hidden Vaults taucht als Klartext in der Datei auf!" ); let needle_payload = b"TOP SECRET INTELLIGENCE DATA"; assert!( !raw_bytes.windows(needle_payload.len()).any(|w| w == needle_payload), "Forensischer Leak: Nutzlast des Hidden Vaults taucht als Klartext auf!" ); // 7. Storage Compaction (Incremental Vacuum & Chunk Shredding) // Große temporäre Datei im Hidden Vault anlegen let temp_large_path = DavPath::new("/large_dump.dat").unwrap(); let mut opts_dump = OpenOptions::default(); opts_dump.write = true; opts_dump.create_new = true; let mut dump_file = fs_hidden.open(&temp_large_path, opts_dump).await.expect("Open dump"); let mut large_buffer = vec![0u8; 512 * 1024]; // 512 KB echte Zufallsdaten (Shannon-Entropie 8.0) rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut large_buffer); dump_file.write_bytes(Bytes::copy_from_slice(&large_buffer)).await.expect("Write dump"); dump_file.flush().await.expect("Flush dump"); drop(dump_file); db.checkpoint().expect("Checkpoint after write"); // Datei löschen (löst automatisches Chunk-Shredding aus) fs_hidden.remove_file(&temp_large_path).await.expect("Remove dump"); db.checkpoint().expect("Checkpoint after remove"); let free_pages = db.freelist_count().expect("freelist count"); assert!(free_pages > 0, "Nach dem Löschen müssen freie Seiten in der Freelist existieren"); // Incremental Vacuum ausführen let reclaimed = db.incremental_vacuum(None).expect("incremental vacuum"); assert!(reclaimed > 0, "Seiten müssen an das Dateisystem zurückgegeben werden"); assert_eq!(db.freelist_count().unwrap(), 0, "Freelist muss jetzt 0 sein"); // Verifiziere, dass verbleibende Dateien in beiden Vaults intakt lesbar sind let mut r_decoy = fs_decoy.open(&decoy_file_path, OpenOptions::default()).await.expect("open decoy"); let d_data = r_decoy.read_bytes(30).await.expect("read decoy"); assert_eq!(&d_data[..], b"Apples, Flour, Sugar, Butter"); let mut r_hidden = fs_hidden.open(&hidden_file_path, OpenOptions::default()).await.expect("open hidden"); let h_data = r_hidden.read_bytes(30).await.expect("read hidden"); assert_eq!(&h_data[..], b"TOP SECRET INTELLIGENCE DATA"); // Aufräumen let _ = std::fs::remove_file(&container_path); }