Initial commit: Sanctum encrypted single-file container for Windows
This commit is contained in:
+228
@@ -0,0 +1,228 @@
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
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, error, info, warn};
|
||||
|
||||
use crate::crypto::{derive_kek, unwrap_dek};
|
||||
use crate::storage::Database;
|
||||
use crate::vfs::SanctumFs;
|
||||
|
||||
/// 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);
|
||||
info!("Trennen des Netzlaufwerks {} ...", drive_str);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
info!("Laufwerk {} erfolgreich getrennt.", drive_str);
|
||||
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);
|
||||
info!("Verbinde Netzlaufwerk {} mit {} ...", drive_str, url);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
info!("Laufwerk {} erfolgreich eingebunden!", drive_str);
|
||||
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>,
|
||||
password: &str,
|
||||
) -> Result<()> {
|
||||
let drive_str = format_drive(drive_letter);
|
||||
|
||||
if !container_path.exists() {
|
||||
bail!(
|
||||
"Containerdatei '{}' existiert nicht.",
|
||||
container_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Öffne Container '{}' ...",
|
||||
container_path.display()
|
||||
);
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
info!("Lese Header und verifiziere Magic Bytes ...");
|
||||
let meta = db
|
||||
.read_meta()
|
||||
.context("Konnte Container-Header nicht lesen")?;
|
||||
|
||||
info!("Leite KEK via Argon2id ab ...");
|
||||
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
||||
|
||||
info!("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")?;
|
||||
|
||||
// WebDAV Filesystem und Handler konfigurieren
|
||||
let fs = SanctumFs::new(db.clone(), dek);
|
||||
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() => {
|
||||
info!(
|
||||
"Standard-Port {} belegt, wähle dynamischen freien Port ...",
|
||||
port_to_try
|
||||
);
|
||||
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();
|
||||
info!("WebDAV-Server lauscht auf http://{}", bound_addr);
|
||||
|
||||
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() => {
|
||||
info!("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);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("============================================================");
|
||||
println!(" Sanctum Container erfolgreich gemountet!");
|
||||
println!(" Pfad: {}", container_path.display());
|
||||
println!(" Laufwerk: {}", drive_str);
|
||||
println!(" WebDAV URL: http://127.0.0.1:{}/", bound_port);
|
||||
println!(" Drücke [Ctrl+C] zum sauberen Trennen und Schließen.");
|
||||
println!("============================================================");
|
||||
println!();
|
||||
|
||||
// Warten auf Strg+C
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?;
|
||||
|
||||
println!();
|
||||
info!("Beendigungssignal (Ctrl+C) erhalten.");
|
||||
info!("Trennen des Windows-Netzlaufwerks {} ...", drive_str);
|
||||
|
||||
// Automatisches Unmount
|
||||
if let Err(e) = unmount_drive(drive_letter) {
|
||||
warn!("Warnung beim automatischen Unmount: {e}");
|
||||
}
|
||||
|
||||
// HTTP Server beenden
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
|
||||
// SQLite WAL Checkpoint erzwingen
|
||||
info!("Führe SQLite WAL-Checkpoint aus (PRAGMA wal_checkpoint(TRUNCATE)) ...");
|
||||
if let Err(e) = db.checkpoint() {
|
||||
error!("Fehler beim WAL-Checkpoint: {e}");
|
||||
}
|
||||
|
||||
info!("Sanctum Container wurde sicher und vollständig geschlossen.");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user