use std::path::PathBuf; use bytes::Bytes; use dav_server::davpath::DavPath; use dav_server::fs::{DavFileSystem, FsError, OpenOptions, ReadDirMeta}; use rand::rngs::OsRng; use rand::RngCore; use sanctum::crypto::{ derive_kek, generate_dek, generate_salt, wrap_slot0_payload, wrap_slot1_payload, KdfParams, CHUNK_SIZE, }; use sanctum::storage::Database; use sanctum::verify::verify_container; use sanctum::vfs::SanctumFs; fn temp_db_path(prefix: &str) -> PathBuf { let mut path = std::env::temp_dir(); let id: u64 = OsRng.next_u64(); path.push(format!("sanctum_test_{}_{}.sanctum", prefix, id)); path } #[tokio::test] async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() { let path = temp_db_path("carrier_accounting"); let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB (10 Blöcke à 1 MB) let carrier_name = "system_backup.dat"; let pass_decoy = "DecoyPassword2026!"; let pass_hidden = "SuperSecretHiddenPassword2026!"; let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let salt_0 = generate_salt(); let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap(); let dek_0 = generate_dek(); let salt_1 = generate_salt(); let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap(); let dek_1 = generate_dek(); let carrier_node_id = 3i64; let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap(); let (wrapped_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap(); let db = Database::open(&path).expect("Open database"); let created_cid = db .init_schema_with_carrier( &salt_0, &kdf_params, &wrapped_0, &nonce_0, &tag_0, Some(( carrier_name, carrier_size_bytes, &salt_1, &kdf_params, &wrapped_1, &nonce_1, &tag_1, &dek_0, &dek_1, )), ) .expect("Init carrier schema"); db.checkpoint().unwrap(); assert_eq!(created_cid, Some(carrier_node_id)); // 1. Authentifizierung beider Passwörter let meta = db.read_meta().unwrap(); let auth_decoy = meta.authenticate(pass_decoy).expect("Auth decoy"); assert_eq!(auth_decoy.slot_id(), 0); assert_eq!(**auth_decoy.dek(), *dek_0); assert_eq!(auth_decoy.carrier_node_id(), Some(carrier_node_id)); let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden"); assert_eq!(auth_hidden.slot_id(), 1); assert_eq!(**auth_hidden.dek(), *dek_1); assert_eq!(*auth_hidden.carrier_dek().unwrap(), *dek_0); assert_eq!(auth_hidden.carrier_node_id(), Some(carrier_node_id)); // 2. Decoy Mount: Schutz der Alibi-Datei (system_backup.dat) let decoy_fs = SanctumFs::with_carrier( db.clone(), auth_decoy.dek().clone(), auth_decoy.carrier_dek(), auth_decoy.carrier_node_id(), auth_decoy.version(), true, 0, ); let carrier_path = DavPath::new("/system_backup.dat").unwrap(); // Metadaten der Alibi-Datei im Decoy prüfen let carrier_meta = decoy_fs.metadata(&carrier_path).await.expect("Carrier meta"); assert_eq!(carrier_meta.len(), carrier_size_bytes); assert!(!carrier_meta.is_dir()); // Alibi-Datei darf im Decoy-Mount NICHT zum Schreiben geöffnet werden let write_opts = OpenOptions { write: true, ..Default::default() }; assert!( matches!(decoy_fs.open(&carrier_path, write_opts).await, Err(FsError::Forbidden)), "Alibi-Datei darf nicht zum Schreiben geöffnet werden" ); // Alibi-Datei darf im Decoy-Mount NICHT gelöscht werden assert!( matches!(decoy_fs.remove_file(&carrier_path).await, Err(FsError::Forbidden)), "Alibi-Datei darf nicht gelöscht werden" ); // Alibi-Datei darf im Decoy-Mount NICHT umbenannt werden let new_name = DavPath::new("/renamed.iso").unwrap(); assert!( matches!(decoy_fs.rename(&carrier_path, &new_name).await, Err(FsError::Forbidden)), "Alibi-Datei darf nicht umbenannt werden" ); // Alibi-Datei KANN im Decoy-Mount gelesen werden let read_opts = OpenOptions { read: true, ..Default::default() }; let mut file_reader = decoy_fs.open(&carrier_path, read_opts).await.expect("Open read"); let first_mb = file_reader.read_bytes(CHUNK_SIZE).await.expect("Read first chunk"); assert_eq!(first_mb.len(), CHUNK_SIZE); // 3. Hidden Mount: Dateisystem-Operationen innerhalb des Alibi-Carriers let hidden_fs = SanctumFs::with_carrier( db.clone(), auth_hidden.dek().clone(), auth_hidden.carrier_dek(), auth_hidden.carrier_node_id(), auth_hidden.version(), true, 1, ); // Wurzelverzeichnis des Hidden Vault auflisten (anfangs leer) let root_path = DavPath::new("/").unwrap(); let mut stream = hidden_fs .read_dir(&root_path, ReadDirMeta::None) .await .expect("Read dir root"); use futures_util::StreamExt; let mut entries = Vec::new(); while let Some(item) = stream.next().await { entries.push(item.unwrap().name()); } assert!(entries.is_empty(), "Hidden Vault Wurzelverzeichnis muss anfangs leer sein"); // Ordner erstellen let secret_dir = DavPath::new("/Classified").unwrap(); hidden_fs.create_dir(&secret_dir).await.expect("Create Classified dir"); // Datei im Ordner anlegen und schreiben let secret_file_path = DavPath::new("/Classified/passwords.txt").unwrap(); let create_opts = OpenOptions { create: true, write: true, ..Default::default() }; let mut secret_file = hidden_fs .open(&secret_file_path, create_opts) .await .expect("Create secret file"); let secret_content = b"TopSecretCredentials_2026_SanctumCoreSecureVault"; secret_file .write_bytes(Bytes::from_static(secret_content)) .await .expect("Write secret content"); secret_file.flush().await.expect("Flush secret file"); drop(secret_file); // Datei lesen und verifizieren let read_opts = OpenOptions { read: true, ..Default::default() }; let mut read_handle = hidden_fs .open(&secret_file_path, read_opts) .await .expect("Open secret file for read"); let read_data = read_handle.read_bytes(1024).await.expect("Read secret bytes"); assert_eq!(&read_data[..], secret_content); drop(read_handle); // Größere Binärdatei schreiben (über 2 MB = 2 Blöcke) let big_file_path = DavPath::new("/Classified/payload.bin").unwrap(); let mut big_file = hidden_fs .open(&big_file_path, OpenOptions { create: true, write: true, ..Default::default() }) .await .expect("Create big file"); let payload_size = 2 * 1024 * 1024 + 12345; // 2 MB + 12.345 Bytes let mut payload = vec![0u8; payload_size]; OsRng.fill_bytes(&mut payload); big_file.write_bytes(Bytes::copy_from_slice(&payload)).await.expect("Write big payload"); big_file.flush().await.expect("Flush big file"); drop(big_file); // Datei zurücklesen und Bit-für-Bit verifizieren let mut read_big = hidden_fs .open(&big_file_path, OpenOptions { read: true, ..Default::default() }) .await .expect("Open big file"); let read_big_bytes = read_big.read_bytes(payload_size + 100).await.expect("Read big file bytes"); assert_eq!(read_big_bytes.len(), payload_size); assert_eq!(&read_big_bytes[..], &payload[..]); drop(read_big); // Datei umbenennen let renamed_path = DavPath::new("/Classified/renamed_payload.bin").unwrap(); hidden_fs.rename(&big_file_path, &renamed_path).await.expect("Rename file"); assert!(hidden_fs.metadata(&big_file_path).await.is_err()); assert!(hidden_fs.metadata(&renamed_path).await.is_ok()); // Datei löschen (Blöcke werden geshreddert und freigegeben) hidden_fs.remove_file(&renamed_path).await.expect("Remove file"); assert!(hidden_fs.metadata(&renamed_path).await.is_err()); // Checkpoint SQLite db.checkpoint().unwrap(); // 4. CHUNKS-ACCOUNTING-ANGRIFF & INTEGRITÄTSPRÜFUNG // Ein Angreifer besitzt nur das Decoy-Passwort (dek_0). // Er führt eine 100%-ige kryptografische AEAD-Prüfung aller Chunks in der SQLite-Datenbank durch. // ALLE Chunks müssen fehlerfrei unter DEK_0 entschlüsseln! let report = verify_container(&path, Some(&dek_0), true).expect("Verify with DEK_0"); assert!( report.is_healthy(), "Container muss für einen Angreifer mit DEK_0 100% gesund und fehlerfrei sein! Fehler: {:?}", report.errors ); assert_eq!( report.corrupted_chunks, 0, "Chunks-Accounting: Es darf exakt 0 korrupte Chunks unter DEK_0 geben!" ); assert_eq!( report.orphan_nodes, 0, "Es darf keine verwaisten Knoten geben!" ); // Aufräumen let _ = std::fs::remove_file(&path); } #[tokio::test] async fn test_model_a_container_file_size_invariance() { let path = temp_db_path("carrier_size_invariance"); let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB let pass_decoy = "DecoyPass2026!"; let pass_hidden = "HiddenPass2026!"; let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let salt_0 = generate_salt(); let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap(); let dek_0 = generate_dek(); let salt_1 = generate_salt(); let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap(); let dek_1 = generate_dek(); let carrier_node_id = 3i64; let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap(); let (wrapped_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap(); let db = Database::open(&path).expect("Open database"); db.init_schema_with_carrier( &salt_0, &kdf_params, &wrapped_0, &nonce_0, &tag_0, Some(( "virtual_disk.vhdx", carrier_size_bytes, &salt_1, &kdf_params, &wrapped_1, &nonce_1, &tag_1, &dek_0, &dek_1, )), ) .expect("Init carrier schema"); db.checkpoint().unwrap(); // Initiale Dateigröße messen let initial_file_size = std::fs::metadata(&path).unwrap().len(); assert!(initial_file_size >= carrier_size_bytes, "Containergröße muss mindestens 10 MB betragen"); // Hidden Mount öffnen und 4 MB geheime Daten schreiben let meta = db.read_meta().unwrap(); let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden"); let hidden_fs = SanctumFs::with_carrier( db.clone(), auth_hidden.dek().clone(), auth_hidden.carrier_dek(), auth_hidden.carrier_node_id(), auth_hidden.version(), true, 1, ); let test_file = DavPath::new("/large_confidential.pdf").unwrap(); let mut handle = hidden_fs .open(&test_file, OpenOptions { create: true, write: true, ..Default::default() }) .await .expect("Open file"); let mut random_data = vec![0u8; 4 * 1024 * 1024]; // 4 MB OsRng.fill_bytes(&mut random_data); handle.write_bytes(Bytes::copy_from_slice(&random_data)).await.expect("Write 4MB"); handle.flush().await.expect("Flush 4MB"); drop(handle); db.checkpoint().unwrap(); // Dateigröße nach dem Schreiben von 4 MB im Hidden Vault messen let size_after_hidden_writes = std::fs::metadata(&path).unwrap().len(); // Die Dateigröße auf der Festplatte DARF NICHT WACHSEN! // Alle Chunks wurden in vorallokierte Carrier-Blöcke überschrieben. assert_eq!( initial_file_size, size_after_hidden_writes, "Dateigröße auf der Festplatte darf sich beim Schreiben in den Hidden Vault NICHT verändern! Vorher: {}, Nachher: {}", initial_file_size, size_after_hidden_writes ); let _ = std::fs::remove_file(&path); } #[tokio::test] async fn test_carrier_file_drop_and_append_mode() { let path = temp_db_path("carrier_drop_append"); let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB let carrier_name = "test_carrier.iso"; let pass_decoy = "DecoyPassword2026!"; let pass_hidden = "HiddenSecretPassword2026!"; let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let salt_0 = generate_salt(); let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap(); let dek_0 = generate_dek(); let salt_1 = generate_salt(); let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap(); let dek_1 = generate_dek(); let carrier_node_id = 3i64; let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap(); let (wrapped_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap(); let db = Database::open(&path).expect("Open database"); db.init_schema_with_carrier( &salt_0, &kdf_params, &wrapped_0, &nonce_0, &tag_0, Some(( carrier_name, carrier_size_bytes, &salt_1, &kdf_params, &wrapped_1, &nonce_1, &tag_1, &dek_0, &dek_1, )), ) .expect("Init carrier schema"); db.checkpoint().unwrap(); let meta = db.read_meta().unwrap(); let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden"); let hidden_fs = SanctumFs::with_carrier( db.clone(), auth_hidden.dek().clone(), auth_hidden.carrier_dek(), auth_hidden.carrier_node_id(), auth_hidden.version(), true, 1, ); // 1. TEST CarrierFile::drop: Write bytes OHNE expliziten flush(), dann drop(handle) let test_file = DavPath::new("/drop_flush_test.txt").unwrap(); let mut write_handle = hidden_fs .open(&test_file, OpenOptions { create: true, write: true, ..Default::default() }) .await .expect("Open file for write"); let initial_data = b"Hello from unflushed write!"; write_handle.write_bytes(Bytes::from_static(initial_data)).await.expect("Write initial data"); // WICHTIG: KEIN write_handle.flush()! Nur drop: drop(write_handle); // Jetzt Datei wieder lesend öffnen und prüfen, ob Daten durch Drop persistiert wurden let mut read_handle = hidden_fs .open(&test_file, OpenOptions { read: true, ..Default::default() }) .await .expect("Open file for read"); let read_back = read_handle.read_bytes(100).await.expect("Read data back"); assert_eq!(&read_back[..], initial_data, "Drop muss ungeflushte Datenblöcke und Inode automatisch sichern"); drop(read_handle); // 2. TEST O_APPEND: Im Append-Modus öffnen und weitere Daten anhängen let append_data = b" - Appended data at EOF!"; let mut append_handle = hidden_fs .open(&test_file, OpenOptions { write: true, append: true, ..Default::default() }) .await .expect("Open file for append"); append_handle.write_bytes(Bytes::from_static(append_data)).await.expect("Write appended data"); drop(append_handle); // Drop sichert auch hier // Prüfe den vollständigen Dateiinhalt nach Append let mut read_handle_2 = hidden_fs .open(&test_file, OpenOptions { read: true, ..Default::default() }) .await .expect("Open file for read after append"); let full_content = read_handle_2.read_bytes(200).await.expect("Read full content"); let mut expected = Vec::new(); expected.extend_from_slice(initial_data); expected.extend_from_slice(append_data); assert_eq!(&full_content[..], &expected[..], "O_APPEND muss Daten am Dateiende anhängen"); drop(read_handle_2); let _ = std::fs::remove_file(&path); }