- SA-01: Container-DoS / KDF-Amplification Schutz mit Pre-KDF Validierung, max 2 Slots (nur 0 und 1), Slot 0 Pflicht und strikten BLOB-Laengen - SA-02: Release-Signierung in CI entkoppelt (getrennte build und sign-and-release Jobs, Secret-Isolation) - SA-03: Pinned Download-Integritaet fuer minisign.exe in CI via SHA-256 - SA-04: Immutable Action-Pinning (@sha) und Toolchain-Pinning (1.85.0) in CI - SA-05: Session-Token vollstaendig aus URIs verbannt (403 Forbidden bei Vorkommen im Pfad/Query) - SA-06: Constant-Time Token- und Auth-Vergleiche via subtle::ConstantTimeEq - SA-07: Dokumentations-Klarstellung bzgl. logischem Shredding vs. physischer SSD/FTL/CoW-Persistenz
225 lines
7.0 KiB
Rust
225 lines
7.0 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 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);
|
|
}
|