312 lines
10 KiB
Rust
312 lines
10 KiB
Rust
use sanctum::crypto::{
|
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
|
MIN_TIME_COST,
|
|
};
|
|
use sanctum::mount::is_loopback_host;
|
|
use sanctum::storage::Database;
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn test_is_loopback_host_comprehensive() {
|
|
// Gültige IPv4 Loopback Varianten
|
|
assert!(is_loopback_host("127.0.0.1"));
|
|
assert!(is_loopback_host("127.0.0.1:80"));
|
|
assert!(is_loopback_host("127.0.0.1:8080"));
|
|
assert!(is_loopback_host("127.0.0.1:65535"));
|
|
|
|
// Gültige Hostname Loopback Varianten
|
|
assert!(is_loopback_host("localhost"));
|
|
assert!(is_loopback_host("localhost:80"));
|
|
assert!(is_loopback_host("localhost:8443"));
|
|
assert!(is_loopback_host("LOCALHOST"));
|
|
assert!(is_loopback_host("LocalHost:9000"));
|
|
|
|
// Gültige IPv6 Loopback Varianten
|
|
assert!(is_loopback_host("[::1]"));
|
|
assert!(is_loopback_host("[::1]:80"));
|
|
assert!(is_loopback_host("[::1]:8443"));
|
|
assert!(is_loopback_host("::1"));
|
|
|
|
// DNS-Rebinding & Spoofing Angriffe (MÜSSEN abgewiesen werden)
|
|
assert!(!is_loopback_host("127.0.0.1.attacker.com"));
|
|
assert!(!is_loopback_host("localhost.attacker.com"));
|
|
assert!(!is_loopback_host("localhost.evil.org:8080"));
|
|
assert!(!is_loopback_host("notlocalhost"));
|
|
assert!(!is_loopback_host("attacker.com"));
|
|
assert!(!is_loopback_host("attacker.com:127001"));
|
|
assert!(!is_loopback_host("192.168.1.1"));
|
|
assert!(!is_loopback_host("10.0.0.1"));
|
|
assert!(!is_loopback_host("0.0.0.0"));
|
|
assert!(!is_loopback_host(""));
|
|
assert!(!is_loopback_host(" "));
|
|
assert!(!is_loopback_host("foo:bar:baz"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_wal_and_shm_cleanup_on_close() {
|
|
let temp_dir = std::env::temp_dir();
|
|
let container_path: PathBuf =
|
|
temp_dir.join(format!("test_wal_cleanup_{}.sanctum", std::process::id()));
|
|
if container_path.exists() {
|
|
let _ = std::fs::remove_file(&container_path);
|
|
}
|
|
|
|
let salt = generate_salt();
|
|
let kdf_params = KdfParams {
|
|
memory_cost: MIN_MEMORY_COST_KIB,
|
|
time_cost: MIN_TIME_COST,
|
|
parallelism: 1,
|
|
};
|
|
let kek = derive_kek("WalCleanupPassword2026!", &salt, &kdf_params).unwrap();
|
|
let dek = generate_dek();
|
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
|
|
|
let db = Database::open(&container_path).unwrap();
|
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
|
.unwrap();
|
|
|
|
// Erstelle Knoten, um Schreibaktivität im WAL zu erzeugen
|
|
let _ = db.create_node(1, "test_file.txt", false).unwrap();
|
|
db.checkpoint().unwrap();
|
|
drop(db);
|
|
|
|
// Bereinigungslogik (wie in mount.rs unmount) ausführen
|
|
let base_os = container_path.as_os_str().to_os_string();
|
|
let mut wal = base_os.clone();
|
|
wal.push("-wal");
|
|
let _ = std::fs::remove_file(&wal);
|
|
let mut shm = base_os;
|
|
shm.push("-shm");
|
|
let _ = std::fs::remove_file(&shm);
|
|
|
|
assert!(
|
|
!PathBuf::from(wal).exists(),
|
|
"WAL-Datei darf nach sauberem Unmount nicht zurückbleiben"
|
|
);
|
|
assert!(
|
|
!PathBuf::from(shm).exists(),
|
|
"SHM-Datei darf nach sauberem Unmount nicht zurückbleiben"
|
|
);
|
|
|
|
let _ = std::fs::remove_file(&container_path);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mount_security_multi_auth_and_no_token_in_url() {
|
|
use sanctum::mount::{check_basic_auth, check_token_header, uri_contains_token};
|
|
|
|
let session_token = "4f8a12bc90de45f187a23456789abcde";
|
|
let port = 8443;
|
|
let remote_url = format!("http://127.0.0.1:{}/", port);
|
|
|
|
// R-05 & SA-05: Die Remote-URL für Mount-Befehle darf niemals das Session-Token enthalten!
|
|
assert!(
|
|
!remote_url.contains(session_token),
|
|
"Remote URL darf niemals das Session-Token enthalten"
|
|
);
|
|
|
|
// 1. Basic Auth Prüfung (Constant-Time)
|
|
use base64::Engine;
|
|
let auth_header = format!(
|
|
"Basic {}",
|
|
base64::engine::general_purpose::STANDARD.encode(format!("sanctum:{}", session_token))
|
|
);
|
|
assert!(check_basic_auth(&auth_header, session_token));
|
|
|
|
let wrong_auth = format!(
|
|
"Basic {}",
|
|
base64::engine::general_purpose::STANDARD.encode("sanctum:wrong_token_1234567890abcdef")
|
|
);
|
|
assert!(!check_basic_auth(&wrong_auth, session_token));
|
|
|
|
// 2. Token Header Prüfung (Constant-Time)
|
|
let mut headers = hyper::HeaderMap::new();
|
|
headers.insert("X-Sanctum-Token", session_token.parse().unwrap());
|
|
assert!(check_token_header(&headers, session_token));
|
|
|
|
let mut wrong_headers = hyper::HeaderMap::new();
|
|
wrong_headers.insert(
|
|
"X-Sanctum-Token",
|
|
"wrong_token_1234567890abcdef".parse().unwrap(),
|
|
);
|
|
assert!(!check_token_header(&wrong_headers, session_token));
|
|
|
|
// 3. SA-05: Verifikation, dass Token in URI (Pfad oder Query) erkannt und strikt abgewiesen wird
|
|
let uri_path: hyper::Uri = format!("http://127.0.0.1:8443/{}/test.txt", session_token)
|
|
.parse()
|
|
.unwrap();
|
|
assert!(
|
|
uri_contains_token(&uri_path, session_token),
|
|
"Token im Pfad muss erkannt werden"
|
|
);
|
|
|
|
let uri_query: hyper::Uri = format!("http://127.0.0.1:8443/test.txt?token={}", session_token)
|
|
.parse()
|
|
.unwrap();
|
|
assert!(
|
|
uri_contains_token(&uri_query, session_token),
|
|
"Token im Query-String muss erkannt werden"
|
|
);
|
|
|
|
let uri_clean: hyper::Uri = "http://127.0.0.1:8443/test.txt".parse().unwrap();
|
|
assert!(
|
|
!uri_contains_token(&uri_clean, session_token),
|
|
"Saubere URI darf kein Token enthalten"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_vfs_memory_lock_retention_on_clone_v03() {
|
|
use dav_server::davpath::DavPath;
|
|
use dav_server::fs::{DavFileSystem, ReadDirMeta};
|
|
use futures_util::StreamExt;
|
|
use sanctum::vfs::SanctumFs;
|
|
|
|
let temp_dir = std::env::temp_dir();
|
|
let container_path: PathBuf =
|
|
temp_dir.join(format!("test_v03_lock_{}.sanctum", std::process::id()));
|
|
if container_path.exists() {
|
|
let _ = std::fs::remove_file(&container_path);
|
|
}
|
|
|
|
let salt = generate_salt();
|
|
let kdf_params = KdfParams {
|
|
memory_cost: MIN_MEMORY_COST_KIB,
|
|
time_cost: MIN_TIME_COST,
|
|
parallelism: 1,
|
|
};
|
|
let kek = derive_kek("TestPassV03!", &salt, &kdf_params).unwrap();
|
|
let dek = generate_dek();
|
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
|
|
|
let db = Database::open(&container_path).unwrap();
|
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
|
.unwrap();
|
|
|
|
// 1. Initialisiere SanctumFs
|
|
let fs = SanctumFs::new(db.clone(), dek, sanctum::crypto::FORMAT_VERSION);
|
|
assert_eq!(
|
|
fs.dek_strong_count(),
|
|
1,
|
|
"Anfangs muss genau 1 Referenz auf den verriegelten DEK existieren"
|
|
);
|
|
|
|
// 2. Klone SanctumFs (wie bei jeder HTTP/WebDAV-Anfrage im Server)
|
|
let fs_clone = fs.clone();
|
|
assert_eq!(
|
|
fs.dek_strong_count(),
|
|
2,
|
|
"Nach dem Klonen müssen 2 Referenzen existieren"
|
|
);
|
|
|
|
// 3. Droppe den Klon
|
|
drop(fs_clone);
|
|
|
|
// 4. V-03: Der Refcount muss nun wieder 1 sein. Der Speicherbereich darf NICHT
|
|
// vorzeitig über Drop eines Klons entriegelt worden sein!
|
|
assert_eq!(
|
|
fs.dek_strong_count(),
|
|
1,
|
|
"Nach Drop des Klons muss genau 1 Referenz erhalten bleiben"
|
|
);
|
|
|
|
// Verifiziere funktionale Nutzbarkeit nach Drop des Klons
|
|
let root_path = DavPath::new("/").unwrap();
|
|
let mut entries = fs.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
|
assert!(entries.next().await.is_none());
|
|
|
|
drop(fs);
|
|
let _ = std::fs::remove_file(&container_path);
|
|
}
|
|
|
|
#[test]
|
|
fn test_z01_decoy_password_zeroize_memory() {
|
|
use sanctum::mount::ContainerAuth;
|
|
use zeroize::Zeroizing;
|
|
|
|
// Test that ContainerAuth properly encapsulates Zeroizing credentials
|
|
let raw_pass = "TopSecretDecoyPass2026!".to_string();
|
|
let zeroized_pass = Zeroizing::new(raw_pass.clone());
|
|
let auth = ContainerAuth::Password(zeroized_pass.clone());
|
|
|
|
if let ContainerAuth::Password(ref p) = auth {
|
|
assert_eq!(p.as_str(), raw_pass.as_str());
|
|
} else {
|
|
panic!("ContainerAuth muss Password-Variante enthalten");
|
|
}
|
|
|
|
// Verify Zeroizing cleans memory when dropped
|
|
let ephemeral = Zeroizing::new(String::from("DecoyPassInHeap"));
|
|
assert_eq!(&*ephemeral, "DecoyPassInHeap");
|
|
// Explicit drop calls zeroize
|
|
drop(ephemeral);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_z04_webdav_quota_report() {
|
|
use dav_server::fs::DavFileSystem;
|
|
use sanctum::vfs::SanctumFs;
|
|
|
|
let temp_dir = std::env::temp_dir();
|
|
let container_path: PathBuf =
|
|
temp_dir.join(format!("test_z04_quota_{}.sanctum", std::process::id()));
|
|
if container_path.exists() {
|
|
let _ = std::fs::remove_file(&container_path);
|
|
}
|
|
|
|
let salt = generate_salt();
|
|
let kdf_params = KdfParams {
|
|
memory_cost: MIN_MEMORY_COST_KIB,
|
|
time_cost: MIN_TIME_COST,
|
|
parallelism: 1,
|
|
};
|
|
let kek = derive_kek("TestPassZ04!", &salt, &kdf_params).unwrap();
|
|
let dek = generate_dek();
|
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
|
|
|
let db = Database::open(&container_path).unwrap();
|
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
|
.unwrap();
|
|
|
|
// Erstelle einen Testknoten mit Chunks
|
|
let node = db.create_node(1, "payload.bin", false).unwrap();
|
|
let dummy_chunk = vec![0xAAu8; 1024 * 1024]; // 1 MiB
|
|
db.write_chunk_and_update_size(
|
|
node.id,
|
|
0,
|
|
0,
|
|
&[0u8; 12],
|
|
&[0u8; 16],
|
|
&dummy_chunk,
|
|
1024 * 1024,
|
|
123456,
|
|
)
|
|
.unwrap();
|
|
|
|
let fs = SanctumFs::new(db.clone(), dek, sanctum::crypto::FORMAT_VERSION);
|
|
|
|
// Rufe WebDAV get_quota ab
|
|
let (used, total_opt) = fs.get_quota().await.unwrap();
|
|
|
|
assert!(
|
|
used > 0,
|
|
"Z-04: Verwendeter Speicherplatz muss > 0 sein (gemeldet: {} Bytes)",
|
|
used
|
|
);
|
|
assert!(
|
|
total_opt.is_some(),
|
|
"Z-04: Gesamtkapazität muss gemeldet werden"
|
|
);
|
|
let total = total_opt.unwrap();
|
|
assert!(
|
|
total >= used,
|
|
"Z-04: Gesamtkapazität ({}) muss mindestens dem belegten Speicher ({}) entsprechen",
|
|
total,
|
|
used
|
|
);
|
|
|
|
drop(fs);
|
|
drop(db);
|
|
let _ = std::fs::remove_file(&container_path);
|
|
}
|