use sanctum::sync::validate_node_name; #[test] fn test_validate_node_name_rejections() { // 1. Leere Namen assert!(validate_node_name("").is_err()); // 2. Relative Verzeichnisreferenzen assert!(validate_node_name(".").is_err()); assert!(validate_node_name("..").is_err()); // 3. Pfadtrenner assert!(validate_node_name("foo/bar").is_err()); assert!(validate_node_name("foo\\bar").is_err()); assert!(validate_node_name("../evil.exe").is_err()); assert!(validate_node_name("..\\evil.exe").is_err()); assert!(validate_node_name("/root_file").is_err()); // 4. Null-Bytes assert!(validate_node_name("test\0file.txt").is_err()); // 5. Steuerzeichen (< 0x20) assert!(validate_node_name("line\nbreak.txt").is_err()); assert!(validate_node_name("carriage\rreturn.txt").is_err()); assert!(validate_node_name("tab\tseparated.txt").is_err()); assert!(validate_node_name("control\x01char.txt").is_err()); // 6. Windows reservierte Gerätenamen let reserved_names = [ "CON", "con", "prn", "PRN", "aux", "AUX", "nul", "NUL", "COM1", "com2", "COM9", "lpt1", "LPT2", "LPT9", "con.txt", "aux.dat", "NUL.tar.gz", "com1.log", "prn.pdf", ]; for res in reserved_names { assert!( validate_node_name(res).is_err(), "Reservierter Name '{}' muss abgewiesen werden", res ); } } #[test] fn test_validate_node_name_accepted() { let valid_names = [ "document.pdf", "my_notes_2026.txt", "archive.tar.gz", "spaces are allowed.docx", "unicode_äöü_ß_test.rs", "hyphens-and_underscores.json", "file (1).jpg", "normal_file", ]; for valid in valid_names { assert!( validate_node_name(valid).is_ok(), "Gültiger Name '{}' sollte akzeptiert werden", valid ); } } #[test] fn test_storage_create_and_rename_reject_invalid_names() { let mut path = std::env::temp_dir(); path.push(format!( "sanctum_test_pathutil_{}.sanctum", std::process::id() )); if path.exists() { let _ = std::fs::remove_file(&path); } let salt = sanctum::crypto::generate_salt(); let kdf_params = sanctum::crypto::KdfParams { memory_cost: sanctum::crypto::MIN_MEMORY_COST_KIB, time_cost: sanctum::crypto::MIN_TIME_COST, parallelism: 1, }; let kek = sanctum::crypto::derive_kek("Pass1234!", &salt, &kdf_params).unwrap(); let dek = sanctum::crypto::generate_dek(); let (wrapped, nonce, tag) = sanctum::crypto::wrap_dek(&kek, &dek).unwrap(); let db = sanctum::storage::Database::open(&path).unwrap(); db.init_schema(&salt, &kdf_params, &wrapped, &nonce, &tag) .unwrap(); // create_node_in_vault mit ungültigem Namen muss scheitern assert!(db.create_node_in_vault(0, 1, "..", false, &dek).is_err()); assert!(db.create_node_in_vault(0, 1, "CON", false, &dek).is_err()); assert!(db .create_node_in_vault(0, 1, "sub/dir", false, &dek) .is_err()); // Gültige Datei erstellen let valid_node = db .create_node_in_vault(0, 1, "valid.txt", false, &dek) .unwrap(); // rename_node_in_vault mit ungültigem Namen muss scheitern assert!(db .rename_node_in_vault(valid_node.id, 1, "..", 0, &dek) .is_err()); assert!(db .rename_node_in_vault(valid_node.id, 1, "AUX", 0, &dek) .is_err()); assert!(db .rename_node_in_vault(valid_node.id, 1, "bad\\name", 0, &dek) .is_err()); let _ = std::fs::remove_file(&path); } #[tokio::test] async fn test_carrier_fs_rejects_invalid_names() { use dav_server::davpath::DavPath; use dav_server::fs::{DavFileSystem, FsError, OpenOptions}; use rand::rngs::OsRng; use rand::RngCore; use sanctum::carrier::CarrierFs; 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 std::sync::Arc; let mut path = std::env::temp_dir(); let id: u64 = OsRng.next_u64(); path.push(format!("sanctum_test_carrier_pathval_{}.sanctum", id)); if path.exists() { let _ = std::fs::remove_file(&path); } let carrier_size_bytes = 10 * 1024 * 1024; let carrier_name = "system_backup.dat"; 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"); 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"); let cfs = CarrierFs::load( db.clone(), carrier_node_id, Arc::new(dek_0), Arc::new(dek_1), 3, false, ) .expect("Load CarrierFs"); // 1. create_dir mit ungültigem Namen (Windows-Reservierung CON, PRN) let p_con = DavPath::new("/CON").unwrap(); assert_eq!(cfs.create_dir(&p_con).await, Err(FsError::Forbidden)); let p_aux = DavPath::new("/aux.txt").unwrap(); assert_eq!(cfs.create_dir(&p_aux).await, Err(FsError::Forbidden)); // 2. open (create) mit ungültigem Namen let p_nul = DavPath::new("/NUL").unwrap(); let mut opt_create = OpenOptions::default(); opt_create.create = true; opt_create.write = true; assert_eq!( cfs.open(&p_nul, opt_create.clone()).await.err(), Some(FsError::Forbidden) ); let p_ctrl = DavPath::new("/prn.pdf").unwrap(); assert_eq!( cfs.open(&p_ctrl, opt_create.clone()).await.err(), Some(FsError::Forbidden) ); // 3. Gültige Datei erstellen let p_valid = DavPath::new("/valid.txt").unwrap(); let file = cfs.open(&p_valid, opt_create).await; assert!(file.is_ok(), "Valid file should be created"); drop(file); // 4. rename mit ungültigem Zielnamen let p_com1 = DavPath::new("/COM1.txt").unwrap(); assert_eq!(cfs.rename(&p_valid, &p_com1).await, Err(FsError::Forbidden)); // 5. copy mit ungültigem Zielnamen let p_lpt1 = DavPath::new("/LPT1").unwrap(); assert_eq!(cfs.copy(&p_valid, &p_lpt1).await, Err(FsError::Forbidden)); let _ = std::fs::remove_file(&path); } #[tokio::test] async fn test_vfs_rejects_invalid_names() { use dav_server::davpath::DavPath; use dav_server::fs::{DavFileSystem, FsError, OpenOptions}; use rand::rngs::OsRng; use rand::RngCore; use sanctum::crypto::{ derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION, MIN_MEMORY_COST_KIB, MIN_TIME_COST, }; use sanctum::storage::Database; use sanctum::vfs::SanctumFs; let mut path = std::env::temp_dir(); let id: u64 = OsRng.next_u64(); path.push(format!("sanctum_test_vfs_pathval_{}.sanctum", id)); if path.exists() { let _ = std::fs::remove_file(&path); } let password = "TestPassword1234!"; let kdf_params = KdfParams { memory_cost: MIN_MEMORY_COST_KIB, time_cost: MIN_TIME_COST, parallelism: 1, }; let salt = generate_salt(); let kek = derive_kek(password, &salt, &kdf_params).unwrap(); let dek = generate_dek(); let (wrapped, nonce, tag) = wrap_dek(&kek, &dek).unwrap(); let db = Database::open(&path).unwrap(); db.init_schema(&salt, &kdf_params, &wrapped, &nonce, &tag) .unwrap(); db.set_active_slot_and_dek(0, dek.clone()); let fs = SanctumFs::new(db.clone(), dek, FORMAT_VERSION); // 1. create_dir mit ungültigem Namen let p_con = DavPath::new("/CON").unwrap(); assert_eq!(fs.create_dir(&p_con).await, Err(FsError::Forbidden)); let p_aux = DavPath::new("/aux.txt").unwrap(); assert_eq!(fs.create_dir(&p_aux).await, Err(FsError::Forbidden)); // 2. open (create) mit ungültigem Namen let p_nul = DavPath::new("/NUL").unwrap(); let mut opt_create = OpenOptions::default(); opt_create.create = true; opt_create.write = true; assert_eq!( fs.open(&p_nul, opt_create.clone()).await.err(), Some(FsError::Forbidden) ); let p_prn = DavPath::new("/prn.pdf").unwrap(); assert_eq!( fs.open(&p_prn, opt_create.clone()).await.err(), Some(FsError::Forbidden) ); // 3. Gültige Datei erstellen let p_valid = DavPath::new("/valid.txt").unwrap(); let file = fs.open(&p_valid, opt_create).await; assert!(file.is_ok(), "Valid file should be created"); drop(file); // 4. rename mit ungültigem Zielnamen let p_com1 = DavPath::new("/COM1.txt").unwrap(); assert_eq!(fs.rename(&p_valid, &p_com1).await, Err(FsError::Forbidden)); // 5. copy mit ungültigem Zielnamen let p_lpt1 = DavPath::new("/LPT1").unwrap(); assert_eq!(fs.copy(&p_valid, &p_lpt1).await, Err(FsError::Forbidden)); drop(fs); drop(db); let _ = std::fs::remove_file(&path); }