use std::convert::Infallible; use std::net::SocketAddr; use std::path::Path; 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), RecoveryKey(Zeroizing), } /// 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 `net use : /delete /y`. pub fn unmount_drive(drive_letter: char) -> Result<()> { let drive_str = format_drive(drive_letter); let output = Command::new("net") .args(["use", &drive_str, "/delete", "/y"]) .output() .context("Fehler beim Ausführen des Befehls 'net use'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); bail!( "Netzlaufwerk {} konnte nicht getrennt werden:\n{}{}", drive_str, stdout, stderr ); } Ok(()) } /// Bindet ein Windows-Netzlaufwerk via `net use : http://127.0.0.1:/ /persistent:no` ein. fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> { let url = format!("http://127.0.0.1:{}/", port); let output = Command::new("net") .args(["use", drive_str, &url, "/persistent:no"]) .output() .context("Fehler beim Ausführen des Befehls 'net use'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); let webclient_hint = if stderr.contains("67") || stderr.contains("Netzwerkname") || stderr.contains("Systemfehler") { "\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'." } else { "" }; bail!( "Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}", drive_str, stdout, stderr, webclient_hint ); } Ok(()) } /// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk ein. pub async fn mount_container( container_path: &Path, drive_letter: char, requested_port: Option, auth: ContainerAuth, open_explorer: bool, enable_tray: bool, idle_timeout: Option, lock_on_screen_lock: bool, anti_leak: bool, ) -> Result<()> { let drive_str = format_drive(drive_letter); if !container_path.exists() { bail!( "Containerdatei '{}' existiert nicht.", container_path.display() ); } 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) => { ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)..."); match meta.authenticate(password) { Some(keys) => { 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) => { ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel..."); let dek = mnemonic_to_dek(phrase) .context("Ungültiger 24-Wort Notfallschlüssel")?; ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!"); // Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann let is_hidden = { let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default(); !children.is_empty() }; let vault_id = if is_hidden { 1 } else { 0 }; (dek, None, None, meta.version, vault_id) } }; // 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(); ui::step( 4, 4, "🌐", &format!( "Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...", bound_port, drive_str ), ); let (shutdown_tx, mut shutdown_rx) = watch::channel(false); // Hyper HTTP Server Loop im Hintergrund starten let server_dav = dav_server.clone(); let server_handle = tokio::spawn(async move { loop { tokio::select! { res = listener.accept() => { let (stream, _) = match res { Ok(val) => val, Err(e) => { warn!("Verbindungsfehler im TCP-Listener: {e}"); continue; } }; let io = TokioIo::new(stream); let handler = server_dav.clone(); tokio::spawn(async move { let service = service_fn(move |req| { let h = handler.clone(); async move { Ok::<_, Infallible>(h.handle(req).await) } }); if let Err(err) = http1::Builder::new().serve_connection(io, service).await { // Client-Disconnects im Explorer sind normal debug!("HTTP-Verbindungsende: {:?}", err); } }); } _ = shutdown_rx.changed() => { debug!("WebDAV-Server-Task empfängt Shutdown-Signal."); break; } } } }); // Netzlaufwerk einbinden if let Err(e) = run_net_use_mount(&drive_str, bound_port) { let _ = shutdown_tx.send(true); let _ = server_handle.await; return Err(e); } // Optional automatisch im Windows Explorer öffnen if open_explorer { let _ = crate::windows::open_in_explorer(drive_letter); } // System-Tray Initialisierung 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) let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1); 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 }; println!(); println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ ✔ Sanctum Container erfolgreich gemountet │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(); println!(" • Container: {}", container_path.display()); println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str)); println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port); if vault_id == 1 { println!(" • Vault: {} (Verschlüsselte Dateinamen)", ui::magenta("Hidden Vault (Slot 1)")); } if let Some(secs) = idle_timeout { println!(" • Auto-Lock: Inaktivität nach {}s", secs); } if lock_on_screen_lock { println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv"); } if anti_leak { println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)"); } if enable_tray { println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)"); } println!(); println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp")); println!(); // Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L) tokio::select! { res = tokio::signal::ctrl_c() => { let _ = res; println!(); println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); } _ = tray_shutdown_rx.recv() => { println!(); println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]")); } _ = session_lock_rx.recv() => { println!(); println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]")); } _ = idle_shutdown_rx.recv() => { println!(); println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]")); } } 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")); } // 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!(); Ok(()) }