368 lines
13 KiB
Rust
368 lines
13 KiB
Rust
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 crate::crypto::{derive_kek, mnemonic_to_dek, unwrap_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(String),
|
|
RecoveryKey(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 `net use <DRIVE>: /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 <DRIVE>: http://127.0.0.1:<PORT>/ /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);
|
|
bail!(
|
|
"Laufwerk {} konnte nicht eingebunden werden:\n{}{}",
|
|
drive_str,
|
|
stdout,
|
|
stderr
|
|
);
|
|
}
|
|
|
|
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<u16>,
|
|
auth: ContainerAuth,
|
|
open_explorer: bool,
|
|
enable_tray: bool,
|
|
idle_timeout: Option<u64>,
|
|
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, version) = match auth {
|
|
ContainerAuth::Password(ref password) => {
|
|
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
|
|
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
|
.context("Schlüsselableitung fehlgeschlagen")?;
|
|
|
|
ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM...");
|
|
let dek = unwrap_dek(
|
|
&kek,
|
|
&meta.wrapped_dek,
|
|
&meta.header_nonce,
|
|
&meta.header_tag,
|
|
)
|
|
.context("Ungültiges Master-Passwort oder Container beschädigt")?;
|
|
(dek, meta.version)
|
|
}
|
|
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!");
|
|
(dek, meta.version)
|
|
}
|
|
};
|
|
|
|
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield)
|
|
let fs = SanctumFs::with_options(db.clone(), dek, version, anti_leak);
|
|
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 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;
|
|
|
|
// 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(())
|
|
}
|
|
|