990 lines
36 KiB
Rust
990 lines
36 KiB
Rust
use std::convert::Infallible;
|
|
use std::net::SocketAddr;
|
|
use std::path::Path;
|
|
#[cfg(unix)]
|
|
use std::process::Command;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
use dav_server::{fakels::FakeLs, DavHandler};
|
|
use hyper::server::conn::http1;
|
|
use hyper::service::service_fn;
|
|
use hyper_util::rt::TokioIo;
|
|
use tokio::net::TcpListener;
|
|
use tokio::sync::watch;
|
|
use tracing::{debug, warn};
|
|
use zeroize::Zeroizing;
|
|
|
|
use crate::crypto::mnemonic_to_dek;
|
|
use crate::storage::Database;
|
|
use crate::ui;
|
|
use crate::vfs::SanctumFs;
|
|
|
|
/// Authentifizierungsmethode für das Einbinden eines Containers: Entweder Master-Passwort oder 24-Wort Notfallschlüssel.
|
|
#[derive(Debug, Clone)]
|
|
pub enum ContainerAuth {
|
|
Password(Zeroizing<String>),
|
|
RecoveryKey(Zeroizing<String>),
|
|
}
|
|
|
|
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
|
|
pub fn format_drive(drive_letter: char) -> String {
|
|
format!("{}:", drive_letter.to_ascii_uppercase())
|
|
}
|
|
|
|
/// Trennt ein Windows-Netzlaufwerk via WNetCancelConnection2W bzw. Unix-Mountpoint via gio.
|
|
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
|
#[cfg(windows)]
|
|
{
|
|
crate::windows::unmount_drive_wnet(drive_letter)
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
let _ = drive_letter;
|
|
Ok(())
|
|
}
|
|
#[cfg(not(any(windows, unix)))]
|
|
{
|
|
let _ = drive_letter;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Bindet ein Windows-Netzlaufwerk via in-process WNetAddConnection2W ein (R-05, kein Token in argv)
|
|
/// bzw. unter Unix via `gio mount dav://127.0.0.1:<PORT>/`.
|
|
fn run_mount_command(drive_letter: char, port: u16, session_token: &str) -> Result<()> {
|
|
#[cfg(windows)]
|
|
{
|
|
crate::windows::mount_drive_wnet(drive_letter, port, session_token)
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
let dav_url = format!("dav://127.0.0.1:{}/", port);
|
|
let _ = Command::new("gio").args(["mount", &dav_url]).output();
|
|
let _ = (drive_letter, session_token);
|
|
Ok(())
|
|
}
|
|
#[cfg(not(any(windows, unix)))]
|
|
{
|
|
let _ = (drive_letter, port, session_token);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk (Windows) bzw. VFS (Linux) ein.
|
|
pub async fn mount_container(
|
|
container_path: &Path,
|
|
drive_letter: char,
|
|
mount_point: Option<&Path>,
|
|
requested_port: Option<u16>,
|
|
auth: ContainerAuth,
|
|
open_explorer: bool,
|
|
enable_tray: bool,
|
|
idle_timeout: Option<u64>,
|
|
lock_on_screen_lock: bool,
|
|
anti_leak: bool,
|
|
stealth: bool,
|
|
) -> Result<()> {
|
|
let drive_str = format_drive(drive_letter);
|
|
let _ = &drive_str;
|
|
let _ = mount_point;
|
|
let _ = open_explorer;
|
|
let _ = enable_tray;
|
|
let _ = lock_on_screen_lock;
|
|
|
|
if !container_path.exists() {
|
|
bail!(
|
|
"Containerdatei '{}' existiert nicht.",
|
|
container_path.display()
|
|
);
|
|
}
|
|
|
|
if !stealth {
|
|
println!();
|
|
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
|
}
|
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
|
|
|
let meta = db
|
|
.read_meta()
|
|
.context("Konnte Container-Header nicht lesen")?;
|
|
|
|
let (dek, carrier_dek, carrier_node_id, version, vault_id) = match auth {
|
|
ContainerAuth::Password(ref password) => {
|
|
if !stealth {
|
|
ui::step(
|
|
2,
|
|
4,
|
|
"🔑",
|
|
"Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...",
|
|
);
|
|
}
|
|
match meta.authenticate(password) {
|
|
Some(keys) => {
|
|
if !stealth {
|
|
ui::step(
|
|
3,
|
|
4,
|
|
"🔓",
|
|
"Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!",
|
|
);
|
|
}
|
|
let vault_id = keys.slot_id();
|
|
let ver = keys.version();
|
|
let c_dek = keys.carrier_dek();
|
|
let c_nid = keys.carrier_node_id();
|
|
(keys.0, c_dek, c_nid, ver, vault_id)
|
|
}
|
|
None => {
|
|
bail!("Ungültiges Master-Passwort oder Container beschädigt");
|
|
}
|
|
}
|
|
}
|
|
ContainerAuth::RecoveryKey(ref phrase) => {
|
|
if !stealth {
|
|
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
|
}
|
|
let dek = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
|
if !stealth {
|
|
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
|
}
|
|
|
|
let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0);
|
|
let (carrier_dek, final_carrier_node_id, vault_id) = if carrier_node_id > 0 {
|
|
let slot_id = crate::recovery::detect_recovery_key_slot(&db, &dek, meta.version)?;
|
|
if slot_id == 1 {
|
|
if !stealth {
|
|
println!(" [i] Der angegebene Notfallschlüssel gehört zum Hidden-Vault (Slot 1).");
|
|
println!(" Für den Zugriff auf die Trägerdatei wird das Passwort des Standard-Vaults benötigt.");
|
|
}
|
|
let decoy_pass =
|
|
rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
|
.context("Fehler beim Einlesen des Standard-Vault Passworts")?;
|
|
let decoy_keys = meta.authenticate(&decoy_pass).ok_or_else(|| {
|
|
anyhow::anyhow!("Ungültiges Passwort für Standard-Vault.")
|
|
})?;
|
|
(Some(decoy_keys.dek().clone()), Some(carrier_node_id), 1)
|
|
} else {
|
|
(None, Some(carrier_node_id), 0)
|
|
}
|
|
} else {
|
|
(None, None, 0)
|
|
};
|
|
|
|
(
|
|
dek,
|
|
carrier_dek,
|
|
final_carrier_node_id,
|
|
meta.version,
|
|
vault_id,
|
|
)
|
|
}
|
|
};
|
|
|
|
// S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren
|
|
let _advisory_lock = db.acquire_advisory_lock_guard(false)?;
|
|
|
|
// 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren
|
|
let mut token_bytes = [0u8; 16];
|
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
|
|
let session_token = hex::encode(token_bytes);
|
|
|
|
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Carrier-Routing)
|
|
let fs = SanctumFs::with_carrier(
|
|
db.clone(),
|
|
dek,
|
|
carrier_dek,
|
|
carrier_node_id,
|
|
version,
|
|
anti_leak,
|
|
vault_id,
|
|
);
|
|
let last_activity = fs.last_activity();
|
|
let dav_server = DavHandler::builder()
|
|
.filesystem(Box::new(fs))
|
|
.locksystem(FakeLs::new())
|
|
.build_handler();
|
|
|
|
// TCP-Port ermitteln und binden
|
|
let port_to_try = requested_port.unwrap_or(8443);
|
|
let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await {
|
|
Ok(l) => l,
|
|
Err(_) if requested_port.is_none() => {
|
|
TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
|
|
.await
|
|
.context("Konnte keinen lokalen TCP-Port binden")?
|
|
}
|
|
Err(e) => {
|
|
bail!("Konnte Port 127.0.0.1:{} nicht binden: {}", port_to_try, e);
|
|
}
|
|
};
|
|
|
|
let bound_addr = listener.local_addr()?;
|
|
let bound_port = bound_addr.port();
|
|
|
|
if !stealth {
|
|
#[cfg(windows)]
|
|
ui::step(
|
|
4,
|
|
4,
|
|
"🌐",
|
|
&format!(
|
|
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
|
|
bound_port, drive_str
|
|
),
|
|
);
|
|
#[cfg(not(windows))]
|
|
ui::step(
|
|
4,
|
|
4,
|
|
"🌐",
|
|
&format!(
|
|
"Starte WebDAV-Server auf Port {} & initialisiere VFS...",
|
|
bound_port
|
|
),
|
|
);
|
|
}
|
|
|
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
|
|
|
// Hyper HTTP Server Loop im Hintergrund starten
|
|
let server_handle = tokio::spawn(serve_webdav_loop(
|
|
listener,
|
|
dav_server,
|
|
session_token.clone(),
|
|
shutdown_rx,
|
|
));
|
|
|
|
// Netzlaufwerk bzw. Verzeichnis einbinden
|
|
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
|
let _ = shutdown_tx.send(true);
|
|
let _ = server_handle.await;
|
|
return Err(e);
|
|
}
|
|
|
|
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
|
#[cfg(windows)]
|
|
if open_explorer {
|
|
let _ = crate::windows::open_in_explorer(drive_letter);
|
|
}
|
|
|
|
// System-Tray Initialisierung (nur Windows)
|
|
#[cfg(windows)]
|
|
let (_tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
|
#[cfg(windows)]
|
|
let _tray = if enable_tray {
|
|
let icon_source = crate::windows::get_default_system_icon()
|
|
.unwrap_or(tray_item::IconSource::Resource(""));
|
|
let title = format!("Sanctum ({drive_str})");
|
|
match tray_item::TrayItem::new(&title, icon_source) {
|
|
Ok(mut tray) => {
|
|
let container_name = container_path
|
|
.file_name()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string();
|
|
let _ = tray.add_label(&format!("Sanctum: {drive_str} ({container_name})"));
|
|
let dl = drive_letter;
|
|
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
|
let _ = crate::windows::open_in_explorer(dl);
|
|
});
|
|
let s_tx = _tray_shutdown_tx.clone();
|
|
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
|
let _ = s_tx.blocking_send(());
|
|
});
|
|
Some(tray)
|
|
}
|
|
Err(e) => {
|
|
debug!("System-Tray Icon konnte nicht erstellt werden: {e}");
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Inaktivitäts-Timer (Auto-Lock)
|
|
let (idle_shutdown_tx, mut idle_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
|
if let Some(timeout_secs) = idle_timeout {
|
|
if timeout_secs > 0 {
|
|
let last_act = last_activity.clone();
|
|
let idle_tx = idle_shutdown_tx.clone();
|
|
tokio::spawn(async move {
|
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
|
loop {
|
|
interval.tick().await;
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
let last = last_act.load(Ordering::Relaxed);
|
|
if now.saturating_sub(last) >= timeout_secs {
|
|
let _ = idle_tx.send(()).await;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
|
|
#[cfg(windows)]
|
|
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
|
#[cfg(windows)]
|
|
let _session_monitor = if lock_on_screen_lock {
|
|
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
|
Ok(guard) => Some(guard),
|
|
Err(e) => {
|
|
warn!("Konnte Windows Session-Lock Monitor nicht aktivieren: {e}");
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if stealth {
|
|
#[cfg(windows)]
|
|
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
|
|
#[cfg(not(windows))]
|
|
println!("Sanctum: WebDAV-Server auf Port {} bereit.", bound_port);
|
|
} else {
|
|
println!();
|
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
|
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
|
println!("└─────────────────────────────────────────────────────────────┘");
|
|
println!();
|
|
println!(" • Container: {}", container_path.display());
|
|
#[cfg(windows)]
|
|
println!(
|
|
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
|
|
ui::cyan(&drive_str)
|
|
);
|
|
#[cfg(not(windows))]
|
|
{
|
|
if let Some(mp) = mount_point {
|
|
println!(" • Mountpoint: {}", ui::cyan(&mp.display().to_string()));
|
|
} else {
|
|
println!(" • Modus: WebDAV Userland-VFS");
|
|
}
|
|
}
|
|
println!(
|
|
" • WebDAV-URL: http://127.0.0.1:{}/ (lokal geschützt)",
|
|
bound_port
|
|
);
|
|
#[cfg(not(windows))]
|
|
{
|
|
for line in format_linux_mount_instructions(bound_port, &session_token, mount_point) {
|
|
println!("{}", line);
|
|
}
|
|
}
|
|
if let Some(secs) = idle_timeout {
|
|
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
|
}
|
|
#[cfg(windows)]
|
|
if lock_on_screen_lock {
|
|
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
|
|
}
|
|
if anti_leak {
|
|
#[cfg(windows)]
|
|
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
|
|
#[cfg(not(windows))]
|
|
println!(" • Anti-Leak: VFS-Metadatenfilter aktiv (.directory, .Trash, desktop.ini blockiert)");
|
|
}
|
|
#[cfg(windows)]
|
|
if enable_tray {
|
|
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
|
}
|
|
println!();
|
|
#[cfg(windows)]
|
|
println!(
|
|
" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.",
|
|
ui::yellow("Tipp")
|
|
);
|
|
#[cfg(not(windows))]
|
|
println!(
|
|
" [{}] Drücke [Ctrl+C] zum sicheren Beenden.",
|
|
ui::yellow("Tipp")
|
|
);
|
|
println!();
|
|
}
|
|
|
|
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT, nur Windows)
|
|
#[cfg(windows)]
|
|
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
|
|
#[cfg(windows)]
|
|
let _console_guard =
|
|
crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok();
|
|
|
|
// Unix Signale (SIGTERM, SIGHUP)
|
|
#[cfg(unix)]
|
|
let mut sigterm =
|
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
|
|
#[cfg(unix)]
|
|
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
|
|
|
|
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
|
|
#[cfg(windows)]
|
|
tokio::select! {
|
|
res = tokio::signal::ctrl_c() => {
|
|
let _ = res;
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
Some(()) = console_close_rx.recv() => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
Some(()) = tray_shutdown_rx.recv() => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
Some(()) = session_lock_rx.recv() => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
Some(()) = idle_shutdown_rx.recv() => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
tokio::select! {
|
|
res = tokio::signal::ctrl_c() => {
|
|
let _ = res;
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
_ = async {
|
|
match sigterm.as_mut() {
|
|
Some(s) => { s.recv().await; }
|
|
None => { std::future::pending::<()>().await; }
|
|
}
|
|
} => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Beendigungssignal (SIGTERM) empfangen.", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
_ = async {
|
|
match sighup.as_mut() {
|
|
Some(s) => { s.recv().await; }
|
|
None => { std::future::pending::<()>().await; }
|
|
}
|
|
} => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Beendigungssignal (SIGHUP) empfangen.", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
Some(()) = idle_shutdown_rx.recv() => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(windows, unix)))]
|
|
tokio::select! {
|
|
res = tokio::signal::ctrl_c() => {
|
|
let _ = res;
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
Some(()) = idle_shutdown_rx.recv() => {
|
|
if !stealth {
|
|
println!();
|
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if stealth {
|
|
let _ = unmount_drive(drive_letter);
|
|
let _ = shutdown_tx.send(true);
|
|
let _ = server_handle.await;
|
|
if let Ok(freelist) = db.freelist_count() {
|
|
if freelist > 0 {
|
|
let _ = db.incremental_vacuum(None);
|
|
}
|
|
}
|
|
let _ = db.checkpoint();
|
|
#[cfg(windows)]
|
|
println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str);
|
|
#[cfg(not(windows))]
|
|
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
|
} else {
|
|
#[cfg(windows)]
|
|
{
|
|
print!(
|
|
" {} Trenne Windows-Netzlaufwerk {} ... ",
|
|
ui::dim("[-]"),
|
|
drive_str
|
|
);
|
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
|
|
// Automatisches Unmount
|
|
if let Err(e) = unmount_drive(drive_letter) {
|
|
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
|
} else {
|
|
println!("{}", ui::green("OK"));
|
|
}
|
|
}
|
|
#[cfg(not(windows))]
|
|
{
|
|
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]"));
|
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
println!("{}", ui::green("OK"));
|
|
}
|
|
|
|
// HTTP Server beenden
|
|
let _ = shutdown_tx.send(true);
|
|
let _ = server_handle.await;
|
|
|
|
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
|
|
if let Ok(freelist) = db.freelist_count() {
|
|
if freelist > 0 {
|
|
print!(
|
|
" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ",
|
|
ui::dim("[-]"),
|
|
freelist
|
|
);
|
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
match db.incremental_vacuum(None) {
|
|
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
|
|
Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))),
|
|
}
|
|
}
|
|
}
|
|
|
|
// SQLite WAL Checkpoint erzwingen
|
|
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
if let Err(e) = db.checkpoint() {
|
|
println!("{}", ui::red(&format!("Fehler ({e})")));
|
|
} else {
|
|
println!("{}", ui::green("OK"));
|
|
}
|
|
|
|
println!();
|
|
println!(
|
|
"{} Sanctum Container wurde sicher und vollständig geschlossen.",
|
|
ui::green("✔")
|
|
);
|
|
println!();
|
|
}
|
|
|
|
drop(db);
|
|
// S-10: Verifikation und Bereinigung von temporären SQLite WAL- und SHM-Dateien
|
|
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);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Überprüft, ob ein Host-Header exakt auf Loopback (127.0.0.1, localhost, [::1], ::1) zeigt (Anti-DNS-Rebinding & S-06).
|
|
/// Striktes Parsing von Host und Port ohne anfällige starts_with-Präfixprüfungen.
|
|
pub fn is_loopback_host(host_str: &str) -> bool {
|
|
let host_trimmed = host_str.trim().to_ascii_lowercase();
|
|
if host_trimmed.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
if host_trimmed == "::1" {
|
|
return true;
|
|
}
|
|
|
|
let host_part = if host_trimmed.starts_with('[') {
|
|
// IPv6 bracketed: [::1] oder [::1]:port
|
|
if let Some(bracket_end) = host_trimmed.find(']') {
|
|
let inside = &host_trimmed[1..bracket_end];
|
|
let after = &host_trimmed[bracket_end + 1..];
|
|
if !after.is_empty() {
|
|
if !after.starts_with(':') {
|
|
return false;
|
|
}
|
|
if after[1..].parse::<u16>().is_err() {
|
|
return false;
|
|
}
|
|
}
|
|
inside
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
// IPv4 oder Hostname: localhost, localhost:8443, 127.0.0.1, 127.0.0.1:8443
|
|
if let Some((h, p)) = host_trimmed.rsplit_once(':') {
|
|
if p.parse::<u16>().is_ok() {
|
|
h
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
&host_trimmed
|
|
}
|
|
};
|
|
|
|
matches!(host_part, "127.0.0.1" | "localhost" | "::1")
|
|
}
|
|
|
|
/// Erzeugt die plattformspezifischen Linux-Mount-Anweisungen und Authentifizierungsdaten (M-03).
|
|
pub fn format_linux_mount_instructions(
|
|
port: u16,
|
|
token: &str,
|
|
mount_point: Option<&std::path::Path>,
|
|
) -> Vec<String> {
|
|
let mut lines = Vec::new();
|
|
lines.push(format!(
|
|
" • WebDAV-Auth: Benutzer: sanctum | Token: {}",
|
|
token
|
|
));
|
|
lines.push(format!(
|
|
" • gio Befehl: echo {} | gio mount dav://sanctum@127.0.0.1:{}/",
|
|
token, port
|
|
));
|
|
if let Some(mp) = mount_point {
|
|
lines.push(format!(
|
|
" • davfs2: mount -t davfs -o username=sanctum http://127.0.0.1:{}/ {}",
|
|
port,
|
|
mp.display()
|
|
));
|
|
} else {
|
|
lines.push(format!(
|
|
" • davfs2: mount -t davfs -o username=sanctum http://127.0.0.1:{}/ /mnt/sanctum",
|
|
port
|
|
));
|
|
}
|
|
lines
|
|
}
|
|
|
|
/// Maximale Anzahl gleichzeitiger Verbindungen zum lokalen WebDAV-Endpunkt (Schutz gegen Socket-Exhaustion).
|
|
const MAX_CONCURRENT_DAV_CONNECTIONS: usize = 64;
|
|
|
|
/// Timeout für das Lesen von HTTP-Headern (Schutz gegen Slowloris-Angriffe auf Loopback).
|
|
const HTTP_HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
|
|
|
/// Hilfsfunktion zur Validierung von HTTP Basic Auth.
|
|
/// Unterstützt Format `username:password` (wobei Passwort dem Session-Token entspricht).
|
|
/// SA-06: Verwendet strikten Constant-Time-Vergleich gegen Timing-Side-Channel-Angriffe.
|
|
pub fn check_basic_auth(auth_header: &str, expected_token: &str) -> bool {
|
|
let auth_str = auth_header.trim();
|
|
let encoded = if let Some(rest) = auth_str.strip_prefix("Basic ") {
|
|
rest
|
|
} else if let Some(rest) = auth_str.strip_prefix("basic ") {
|
|
rest
|
|
} else {
|
|
return false;
|
|
};
|
|
|
|
use base64::Engine;
|
|
let decoded_bytes = match base64::engine::general_purpose::STANDARD.decode(encoded.trim()) {
|
|
Ok(bytes) => bytes,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
let decoded_str = match std::str::from_utf8(&decoded_bytes) {
|
|
Ok(s) => s,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
use subtle::ConstantTimeEq;
|
|
if let Some((_user, pass)) = decoded_str.split_once(':') {
|
|
pass.as_bytes().ct_eq(expected_token.as_bytes()).into()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Hilfsfunktion zur Validierung des X-Sanctum-Token Headers.
|
|
/// SA-06: Verwendet strikten Constant-Time-Vergleich gegen Timing-Side-Channel-Angriffe.
|
|
pub fn check_token_header(headers: &hyper::HeaderMap, expected_token: &str) -> bool {
|
|
use subtle::ConstantTimeEq;
|
|
if let Some(val) = headers.get("X-Sanctum-Token") {
|
|
if let Ok(val_str) = val.to_str() {
|
|
return val_str
|
|
.trim()
|
|
.as_bytes()
|
|
.ct_eq(expected_token.as_bytes())
|
|
.into();
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Prüft, ob eine URI oder deren Query-Parameter das sensible Session-Token enthält (SA-05 / CWE-598).
|
|
pub fn uri_contains_token(uri: &hyper::Uri, session_token: &str) -> bool {
|
|
if session_token.is_empty() {
|
|
return false;
|
|
}
|
|
uri.path().contains(session_token) || uri.query().unwrap_or("").contains(session_token)
|
|
}
|
|
|
|
/// Erzeugt eine standardkonforme HTTP 401 Unauthorized Antwort mit WWW-Authenticate Header.
|
|
fn unauthorized_response() -> hyper::Response<dav_server::body::Body> {
|
|
hyper::Response::builder()
|
|
.status(hyper::StatusCode::UNAUTHORIZED)
|
|
.header(hyper::header::WWW_AUTHENTICATE, "Basic realm=\"Sanctum\"")
|
|
.header(hyper::header::CONTENT_LENGTH, "0")
|
|
.body(dav_server::body::Body::empty())
|
|
.unwrap()
|
|
}
|
|
|
|
/// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Header-basierter Authentifizierung & Host-Header Sicherheits-Middleware aus.
|
|
pub async fn serve_webdav_loop(
|
|
listener: TcpListener,
|
|
dav_server: DavHandler,
|
|
session_token: String,
|
|
mut shutdown_rx: watch::Receiver<bool>,
|
|
) {
|
|
let conn_semaphore =
|
|
std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_DAV_CONNECTIONS));
|
|
|
|
loop {
|
|
tokio::select! {
|
|
res = listener.accept() => {
|
|
let (stream, _) = match res {
|
|
Ok(val) => val,
|
|
Err(e) => {
|
|
warn!("Verbindungsfehler im TCP-Listener: {e}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// RT-01: Schutz gegen Connection-Starvation / Socket-Flooding (CWE-400)
|
|
let permit = match conn_semaphore.clone().try_acquire_owned() {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
warn!(
|
|
"WebDAV-Verbindungslimit ({} aktive Verbindungen) erreicht: Wehre potenziellen Connection-Starvation-Angriff ab.",
|
|
MAX_CONCURRENT_DAV_CONNECTIONS
|
|
);
|
|
drop(stream);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let io = TokioIo::new(stream);
|
|
let handler = dav_server.clone();
|
|
let expected_token = session_token.clone();
|
|
|
|
tokio::spawn(async move {
|
|
let _permit = permit; // Permit wird bei Verbindungsende automatisch freigegeben
|
|
|
|
let service = service_fn(move |req| {
|
|
let h = handler.clone();
|
|
let token = expected_token.clone();
|
|
async move {
|
|
// 1. RT-02: Strikte Fail-Closed Host-Header Validierung (Anti-DNS-Rebinding & Anti-Spoofing)
|
|
let host_valid = match req.headers().get(hyper::header::HOST) {
|
|
Some(host_val) => match host_val.to_str() {
|
|
Ok(host_str) => is_loopback_host(host_str),
|
|
Err(_) => false,
|
|
},
|
|
None => false,
|
|
};
|
|
|
|
if !host_valid {
|
|
warn!(
|
|
"Abgewiesener Zugriff: Fehlender, ungültiger oder externer Host-Header ({:?})",
|
|
req.headers().get(hyper::header::HOST)
|
|
);
|
|
let res = hyper::Response::builder()
|
|
.status(hyper::StatusCode::FORBIDDEN)
|
|
.header(hyper::header::CONTENT_LENGTH, "0")
|
|
.body(dav_server::body::Body::empty())
|
|
.unwrap();
|
|
return Ok::<_, Infallible>(res);
|
|
}
|
|
|
|
// 2. SA-05: Verhindere Session-Token in Request-URI oder Query (CWE-598).
|
|
// Session-Tokens dürfen ausschließlich in HTTP-Headern übertragen werden.
|
|
if uri_contains_token(req.uri(), &token) {
|
|
warn!("Abgewiesener Zugriff: Session-Token in URI/Query übermittelt (SA-05 / CWE-598)");
|
|
let res = hyper::Response::builder()
|
|
.status(hyper::StatusCode::FORBIDDEN)
|
|
.header(hyper::header::CONTENT_LENGTH, "0")
|
|
.body(dav_server::body::Body::empty())
|
|
.unwrap();
|
|
return Ok::<_, Infallible>(res);
|
|
}
|
|
|
|
// 3. SA-05 & SA-06: Header-basierte Authentifizierung mit Constant-Time Token-Vergleich
|
|
// a) HTTP Basic Auth (Authorization: Basic ...)
|
|
// b) Header X-Sanctum-Token
|
|
let mut authenticated = false;
|
|
|
|
if let Some(auth_val) = req.headers().get(hyper::header::AUTHORIZATION) {
|
|
if let Ok(auth_str) = auth_val.to_str() {
|
|
if check_basic_auth(auth_str, &token) {
|
|
authenticated = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !authenticated && check_token_header(req.headers(), &token) {
|
|
authenticated = true;
|
|
}
|
|
|
|
if !authenticated {
|
|
debug!("Abgewiesener unauthentifizierter Zugriff auf: {}", req.uri().path());
|
|
return Ok::<_, Infallible>(unauthorized_response());
|
|
}
|
|
|
|
Ok::<_, Infallible>(h.handle(req).await)
|
|
}
|
|
});
|
|
|
|
// RT-01: Header-Read-Timeout & TokioTimer zur aktiven Abwehr von Slowloris-Hanging-Sockets
|
|
let mut builder = http1::Builder::new();
|
|
builder.timer(hyper_util::rt::TokioTimer::new());
|
|
builder.header_read_timeout(HTTP_HEADER_READ_TIMEOUT);
|
|
|
|
if let Err(err) = builder.serve_connection(io, service).await {
|
|
debug!("HTTP-Verbindungsende: {:?}", err);
|
|
}
|
|
});
|
|
}
|
|
_ = shutdown_rx.changed() => {
|
|
debug!("WebDAV-Server-Task empfängt Shutdown-Signal.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_is_loopback_host_validation() {
|
|
assert!(is_loopback_host("127.0.0.1"));
|
|
assert!(is_loopback_host("127.0.0.1:8443"));
|
|
assert!(is_loopback_host("localhost"));
|
|
assert!(is_loopback_host("localhost:8443"));
|
|
assert!(is_loopback_host("[::1]"));
|
|
assert!(is_loopback_host("[::1]:8443"));
|
|
|
|
// Abweisung externer Hosts oder DNS-Rebinding-Attacken
|
|
assert!(!is_loopback_host("evil.com"));
|
|
assert!(!is_loopback_host("attacker.local"));
|
|
assert!(!is_loopback_host("192.168.1.50"));
|
|
assert!(!is_loopback_host("10.0.0.1"));
|
|
assert!(!is_loopback_host(""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_drive() {
|
|
assert_eq!(format_drive('s'), "S:");
|
|
assert_eq!(format_drive('Z'), "Z:");
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_basic_auth() {
|
|
use base64::Engine;
|
|
let token = "deadbeefcafebabe0123456789abcdef";
|
|
let creds = format!("sanctum:{}", token);
|
|
let header_val = format!(
|
|
"Basic {}",
|
|
base64::engine::general_purpose::STANDARD.encode(creds)
|
|
);
|
|
assert!(check_basic_auth(&header_val, token));
|
|
|
|
// Kleingeschriebenes basic Präfix
|
|
let lower_header = format!(
|
|
"basic {}",
|
|
base64::engine::general_purpose::STANDARD.encode(format!("user:{}", token))
|
|
);
|
|
assert!(check_basic_auth(&lower_header, token));
|
|
|
|
// Falsches Token
|
|
let wrong_token_header = format!(
|
|
"Basic {}",
|
|
base64::engine::general_purpose::STANDARD.encode("sanctum:wrongtoken")
|
|
);
|
|
assert!(!check_basic_auth(&wrong_token_header, token));
|
|
|
|
// Ungültiges Base64 oder Format
|
|
assert!(!check_basic_auth("Basic !!!notbase64!!!", token));
|
|
assert!(!check_basic_auth("Bearer 12345", token));
|
|
assert!(!check_basic_auth("", token));
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_token_header() {
|
|
let token = "deadbeefcafebabe0123456789abcdef";
|
|
let mut headers = hyper::HeaderMap::new();
|
|
assert!(!check_token_header(&headers, token));
|
|
|
|
headers.insert("X-Sanctum-Token", token.parse().unwrap());
|
|
assert!(check_token_header(&headers, token));
|
|
|
|
let mut wrong_headers = hyper::HeaderMap::new();
|
|
wrong_headers.insert("X-Sanctum-Token", "wrong".parse().unwrap());
|
|
assert!(!check_token_header(&wrong_headers, token));
|
|
}
|
|
|
|
#[test]
|
|
fn test_uri_contains_token_rejection() {
|
|
let token = "deadbeefcafebabe0123456789abcdef";
|
|
|
|
// Token im Pfad
|
|
let uri_path: hyper::Uri = format!("http://127.0.0.1:8443/{}/Photos/vacation.jpg", token)
|
|
.parse()
|
|
.unwrap();
|
|
assert!(uri_contains_token(&uri_path, token));
|
|
|
|
// Token im Query-String
|
|
let uri_query: hyper::Uri =
|
|
format!("http://127.0.0.1:8443/Photos/vacation.jpg?token={}", token)
|
|
.parse()
|
|
.unwrap();
|
|
assert!(uri_contains_token(&uri_query, token));
|
|
|
|
// Harmloser Pfad ohne Token
|
|
let uri_clean: hyper::Uri = "http://127.0.0.1:8443/Photos/vacation.jpg".parse().unwrap();
|
|
assert!(!uri_contains_token(&uri_clean, token));
|
|
|
|
// Leeres Token darf niemals matchen
|
|
assert!(!uri_contains_token(&uri_clean, ""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_m03_format_linux_mount_instructions() {
|
|
let token = "fedcba9876543210fedcba9876543210";
|
|
let instructions = format_linux_mount_instructions(
|
|
8443,
|
|
token,
|
|
Some(std::path::Path::new("/mnt/secure")),
|
|
);
|
|
|
|
let all_text = instructions.join("\n");
|
|
assert!(all_text.contains("Benutzer: sanctum"));
|
|
assert!(all_text.contains(token));
|
|
assert!(all_text.contains("gio mount dav://sanctum@127.0.0.1:8443/"));
|
|
assert!(all_text.contains("mount -t davfs -o username=sanctum http://127.0.0.1:8443/ /mnt/secure"));
|
|
}
|
|
}
|