use rand::rngs::OsRng; use rand::RngCore; use std::collections::HashSet; use std::path::PathBuf; use sanctum::crypto::{ derive_kek, generate_dek, generate_salt, wrap_slot0_payload, wrap_slot1_payload, KdfParams, MIN_MEMORY_COST_KIB, MIN_TIME_COST, }; use sanctum::storage::Database; use sanctum::sync::{delete_orphans_in_vault, SyncStats}; 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 } #[test] fn test_carrier_protection_in_storage_and_sync() { let path = temp_db_path("carrier_sec"); let carrier_name = "secret_carrier.iso"; let carrier_size_bytes = 2 * 1024 * 1024; // 2 MB let pass_decoy = "DecoyPassword2026!"; let pass_hidden = "SuperSecretHiddenPassword2026!"; let kdf_params = KdfParams { memory_cost: MIN_MEMORY_COST_KIB, time_cost: MIN_TIME_COST, 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. Direkter Löschversuch der Trägerdatei muss fail-closed abgewehrt werden let del_res = db.delete_node(carrier_node_id); assert!( del_res.is_err(), "Löschen der Trägerdatei muss fehlschlagen" ); assert!(del_res.unwrap_err().to_string().contains("Carrier-Schutz")); // 2. Umbenennung der Trägerdatei muss fail-closed abgewehrt werden let rename_res = db.rename_node_in_vault(carrier_node_id, 1, "renamed.iso", 0, &dek_0); assert!( rename_res.is_err(), "Umbenennen der Trägerdatei muss fehlschlagen" ); assert!(rename_res .unwrap_err() .to_string() .contains("Carrier-Schutz")); // 3. Truncate der Trägerdatei muss fail-closed abgewehrt werden let trunc_res = db.truncate_chunks_after(carrier_node_id, 0); assert!( trunc_res.is_err(), "Truncate der Trägerdatei muss fehlschlagen" ); assert!(trunc_res .unwrap_err() .to_string() .contains("Carrier-Schutz")); // 4. delete_orphans_in_vault mit leerem lokalem Pfad-Set: // Der Carrier darf unter keinen Umständen gelöscht werden! let local_paths = HashSet::new(); let excluded_paths = HashSet::new(); let mut stats = SyncStats::default(); let orphan_res = delete_orphans_in_vault( &db, 0, // Decoy Vault ID &dek_0, 1, // Root-Knoten "", &local_paths, &excluded_paths, &[], false, // delete_excluded: false false, // kein dry_run true, // quiet &mut stats, ); assert!( orphan_res.is_ok(), "delete_orphans_in_vault muss ohne Fehler durchlaufen" ); assert_eq!( stats.files_deleted, 0, "Carrier-Datei darf nicht gelöscht worden sein" ); // Sicherstellen, dass Carrier nach wie vor in der DB existiert let carrier_rec = db.get_node_by_id(carrier_node_id).unwrap(); assert!( carrier_rec.is_some(), "Carrier-Knoten muss nach Sync unversehrt vorhanden sein" ); // 5. R-02: sync_single_file_to_host muss Carrier-Pull strikt ablehnen let carrier_node = db.get_node_by_id(carrier_node_id).unwrap().unwrap(); let local_dest_file = std::env::temp_dir().join(format!("carrier_leak_{}.iso", std::process::id())); let pull_single_res = sanctum::sync::sync_single_file_to_host( &db, 0, &dek_0, sanctum::crypto::FORMAT_VERSION, &carrier_node, &local_dest_file, false, false, ); assert!( pull_single_res.is_err(), "sync_single_file_to_host auf Carrier muss fehlschlagen" ); assert!( !local_dest_file.exists(), "Trägerdatei darf niemals auf den Host geschrieben werden" ); // 6. R-02: run_sync mit Pull muss die Trägerdatei überspringen let local_pull_dir = std::env::temp_dir().join(format!("sanctum_pull_test_{}", std::process::id())); let sync_options = sanctum::sync::SyncOptions { direction: sanctum::sync::SyncDirection::Pull, delete: false, delete_excluded: false, dry_run: false, checksum: false, exclude_patterns: Vec::new(), quiet: true, force: false, }; let pull_res = sanctum::sync::run_sync( &db, 0, &dek_0, sanctum::crypto::FORMAT_VERSION, "/", &local_pull_dir.to_string_lossy(), &sync_options, ); assert!( pull_res.is_ok(), "run_sync pull muss erfolgreich durchlaufen" ); let pulled_carrier = local_pull_dir.join(carrier_name); assert!( !pulled_carrier.exists(), "Trägerdatei darf bei recursive pull nicht auf den Host kopiert werden" ); let _ = std::fs::remove_dir_all(&local_pull_dir); // 7. R-02: VFS Copy Schutz let fs = sanctum::vfs::SanctumFs::with_carrier( db.clone(), dek_0.clone(), Some(dek_0.clone()), Some(carrier_node_id), sanctum::crypto::FORMAT_VERSION, false, 0, ); use dav_server::davpath::DavPath; use dav_server::fs::DavFileSystem; // Copy Quelle = Carrier let from_carrier = DavPath::new(&format!("/{}", carrier_name)).unwrap(); let to_copy = DavPath::new("/carrier_copy.iso").unwrap(); let rt = tokio::runtime::Runtime::new().unwrap(); let copy_src_res = rt.block_on(fs.copy(&from_carrier, &to_copy)); assert!( copy_src_res.is_err(), "SanctumFs::copy mit Carrier als Quelle muss FsError::Forbidden liefern" ); // Copy Ziel = Carrier let _regular_file = db .create_node_in_vault(0, 1, "regular.txt", false, &dek_0) .unwrap(); let from_regular = DavPath::new("/regular.txt").unwrap(); let to_carrier = DavPath::new(&format!("/{}", carrier_name)).unwrap(); let copy_dst_res = rt.block_on(fs.copy(&from_regular, &to_carrier)); assert!( copy_dst_res.is_err(), "SanctumFs::copy mit Carrier als Ziel muss FsError::Forbidden liefern" ); let _ = std::fs::remove_file(&path); }