Files
sanctum/tests/sync_carrier_protection_test.rs
T
harald 1cdb30147b
Sanctum Release / Build & Release (Windows x86_64) (push) Waiting to run
release: v0.7.1 — Security-Patch (R-01 bis R-06)
- R-01: Bereinigung verbliebener Restbehauptungen in Doku und Code (LEGAL.md, README.md, QUICKSTART.md, main.rs, crypto/storage/recovery/verify.rs)
- R-02: Lückenloser Carrier-Schutz in SanctumFs::copy (Quelle & Ziel) und sync (Pull-Skip & Push-Schutz)
- R-03: Shared Dateinamen-Validierung (validate_node_name) in pathutil.rs, durchgesetzt in storage.rs und vfs.rs
- R-04: Fail-closed Release-Packaging & obligatorische Minisign-Signatur in CI (.gitea/workflows/release.yaml) und Scripts
- R-05: Session-Token Beseitigung im argv: In-Process Win32 WNetAddConnection2W/WNetCancelConnection2W, kein gio argv-Token, Multi-Auth HTTP Middleware (Basic Auth, X-Sanctum-Token, Path-Fallback) mit 401 WWW-Authenticate
- R-06: Sofortiges Löschen von SANCTUM_RECOVERY_KEY aus der Prozessumgebung
2026-09-18 19:49:38 +02:00

180 lines
6.6 KiB
Rust

use std::collections::HashSet;
use std::path::PathBuf;
use rand::rngs::OsRng;
use rand::RngCore;
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 mut stats = SyncStats::default();
let orphan_res = delete_orphans_in_vault(
&db,
0, // Decoy Vault ID
&dek_0,
1, // Root-Knoten
"",
&local_paths,
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,
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);
}