Files
sanctum/tests/sync_carrier_protection_test.rs
T

230 lines
7.1 KiB
Rust

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