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