Enhance CLI output: UTF-8 console codepage, VT colors, clean status cards

This commit is contained in:
2026-09-07 16:01:50 +02:00
parent a50dc04bb3
commit 681fb0c032
4 changed files with 194 additions and 64 deletions
+1
View File
@@ -1,4 +1,5 @@
pub mod crypto; pub mod crypto;
pub mod mount; pub mod mount;
pub mod storage; pub mod storage;
pub mod ui;
pub mod vfs; pub mod vfs;
+53 -33
View File
@@ -2,12 +2,12 @@ use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use tracing::info;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams}; use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams};
use sanctum::mount::{format_drive, mount_container, unmount_drive}; use sanctum::mount::{format_drive, mount_container, unmount_drive};
use sanctum::storage::Database; use sanctum::storage::Database;
use sanctum::ui;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "sanctum")] #[command(name = "sanctum")]
@@ -73,10 +73,11 @@ fn handle_init(container_path: &Path) -> Result<()> {
); );
} }
println!("============================================================"); println!("┌─────────────────────────────────────────────────────────────┐");
println!(" Sanctum Container Initialisierung"); println!(" Sanctum — Neuen verschlüsselten Container erstellen │");
println!(" Zieldatei: {}", container_path.display()); println!("└─────────────────────────────────────────────────────────────┘");
println!("============================================================"); println!(" Zieldatei: {}", container_path.display());
println!();
let password = rpassword::prompt_password("Master-Passwort eingeben: ") let password = rpassword::prompt_password("Master-Passwort eingeben: ")
.context("Fehler beim Einlesen des Passworts")?; .context("Fehler beim Einlesen des Passworts")?;
@@ -92,20 +93,21 @@ fn handle_init(container_path: &Path) -> Result<()> {
bail!("Die eingegebenen Passwörter stimmen nicht überein!"); bail!("Die eingegebenen Passwörter stimmen nicht überein!");
} }
info!("Generiere KDF-Salt und leite KEK via Argon2id ab ..."); println!();
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
let salt = generate_salt(); let salt = generate_salt();
let kdf_params = KdfParams::default(); let kdf_params = KdfParams::default();
let kek = derive_kek(&password, &salt, &kdf_params) let kek = derive_kek(&password, &salt, &kdf_params)
.context("KDF-Schlüsselableitung fehlgeschlagen")?; .context("KDF-Schlüsselableitung fehlgeschlagen")?;
info!("Erzeuge kryptografisch sicheren DEK via CSPRNG (OsRng) ..."); ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
let dek = generate_dek(); let dek = generate_dek();
info!("Verschlüssele DEK mit KEK via AES-256-GCM ..."); ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
let (wrapped_dek, header_nonce, header_tag) = let (wrapped_dek, header_nonce, header_tag) =
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?; wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
info!("Initialisiere SQLite-Containerstruktur mit WAL-Modus ..."); ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
let db = Database::open(container_path) let db = Database::open(container_path)
.context("Konnte SQLite-Containerdatei nicht anlegen")?; .context("Konnte SQLite-Containerdatei nicht anlegen")?;
@@ -116,31 +118,23 @@ fn handle_init(container_path: &Path) -> Result<()> {
.context("Fehler beim finalen WAL-Checkpoint")?; .context("Fehler beim finalen WAL-Checkpoint")?;
println!(); println!();
println!("✔ Sanctum-Container erfolgreich initialisiert!"); println!("┌─────────────────────────────────────────────────────────────┐");
println!(" Container: {}", container_path.display()); println!(" ✔ Sanctum-Container erfolgreich initialisiert! │");
println!(" Format: Version 1 (Magic: SANCTUM\\0)"); println!("└─────────────────────────────────────────────────────────────┘");
println!(" KDF: Argon2id (M=64MB, T=3, P=4)"); println!();
println!(" Cipher: AES-256-GCM mit AEAD-Swap-Protection (1-MB Chunks)"); println!(" • Container: {}", container_path.display());
println!(" • Format: Version 1 (Magic: SANCTUM\\0)");
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
println!(" • Cipher: AES-256-GCM (1-MB Chunks, AEAD Swap-Schutz)");
println!();
println!(" Befehl zum Einbinden als Netzlaufwerk:");
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\" --drive S", container_path.display())));
println!(); println!();
println!("Zum Einbinden ausführen:");
println!(
" sanctum mount --path \"{}\" --drive S",
container_path.display()
);
Ok(()) Ok(())
} }
#[tokio::main] async fn run() -> Result<()> {
async fn main() -> Result<()> {
// Tracing / Logging initialisieren (Standard-Filter: info)
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("sanctum=info,dav_server=warn,hyper=warn")),
)
.init();
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
@@ -149,20 +143,46 @@ async fn main() -> Result<()> {
} }
Commands::Mount { path, drive, port } => { Commands::Mount { path, drive, port } => {
let drive_char = parse_drive_letter(&drive)?; let drive_char = parse_drive_letter(&drive)?;
let password = rpassword::prompt_password(format!( let prompt_text = format!(
"Master-Passwort für Container '{}' eingeben: ", "Master-Passwort für Container '{}' eingeben: ",
path.display() path.display()
)) );
.context("Fehler beim Einlesen des Passworts")?; let password = rpassword::prompt_password(prompt_text)
.context("Fehler beim Einlesen des Passworts")?;
mount_container(&path, drive_char, port, &password).await?; mount_container(&path, drive_char, port, &password).await?;
} }
Commands::Unmount { drive } => { Commands::Unmount { drive } => {
let drive_char = parse_drive_letter(&drive)?; let drive_char = parse_drive_letter(&drive)?;
let drive_str = format_drive(drive_char);
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
let _ = std::io::Write::flush(&mut std::io::stdout());
unmount_drive(drive_char)?; unmount_drive(drive_char)?;
println!("✔ Laufwerk {} erfolgreich getrennt.", format_drive(drive_char)); println!("{}", ui::green("OK"));
println!("{} Laufwerk {} erfolgreich getrennt.", ui::green(""), drive_str);
} }
} }
Ok(()) Ok(())
} }
#[tokio::main]
async fn main() {
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren
ui::init_console();
// Tracing / Logging initialisieren (Standard-Filter: warn)
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("sanctum=warn,dav_server=warn,hyper=warn")),
)
.init();
if let Err(err) = run().await {
eprintln!();
eprintln!("{} {:#}", ui::red("✖ Fehler:"), err);
std::process::exit(1);
}
}
+41 -31
View File
@@ -10,10 +10,11 @@ use hyper::service::service_fn;
use hyper_util::rt::TokioIo; use hyper_util::rt::TokioIo;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::watch; use tokio::sync::watch;
use tracing::{debug, error, info, warn}; use tracing::{debug, warn};
use crate::crypto::{derive_kek, unwrap_dek}; use crate::crypto::{derive_kek, unwrap_dek};
use crate::storage::Database; use crate::storage::Database;
use crate::ui;
use crate::vfs::SanctumFs; use crate::vfs::SanctumFs;
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:") /// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
@@ -24,7 +25,6 @@ pub fn format_drive(drive_letter: char) -> String {
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`. /// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`.
pub fn unmount_drive(drive_letter: char) -> Result<()> { pub fn unmount_drive(drive_letter: char) -> Result<()> {
let drive_str = format_drive(drive_letter); let drive_str = format_drive(drive_letter);
info!("Trennen des Netzlaufwerks {} ...", drive_str);
let output = Command::new("net") let output = Command::new("net")
.args(["use", &drive_str, "/delete", "/y"]) .args(["use", &drive_str, "/delete", "/y"])
@@ -42,14 +42,12 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> {
); );
} }
info!("Laufwerk {} erfolgreich getrennt.", drive_str);
Ok(()) Ok(())
} }
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/ /persistent:no` ein. /// 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<()> { fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> {
let url = format!("http://127.0.0.1:{}/", port); let url = format!("http://127.0.0.1:{}/", port);
info!("Verbinde Netzlaufwerk {} mit {} ...", drive_str, url);
let output = Command::new("net") let output = Command::new("net")
.args(["use", drive_str, &url, "/persistent:no"]) .args(["use", drive_str, &url, "/persistent:no"])
@@ -67,7 +65,6 @@ fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> {
); );
} }
info!("Laufwerk {} erfolgreich eingebunden!", drive_str);
Ok(()) Ok(())
} }
@@ -87,23 +84,20 @@ pub async fn mount_container(
); );
} }
info!( println!();
"Öffne Container '{}' ...", ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
container_path.display()
);
let db = Database::open(container_path) let db = Database::open(container_path)
.context("Konnte Container-Datenbank nicht öffnen")?; .context("Konnte Container-Datenbank nicht öffnen")?;
info!("Lese Header und verifiziere Magic Bytes ...");
let meta = db let meta = db
.read_meta() .read_meta()
.context("Konnte Container-Header nicht lesen")?; .context("Konnte Container-Header nicht lesen")?;
info!("Leite KEK via Argon2id ab ..."); ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params) let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
.context("Schlüsselableitung fehlgeschlagen")?; .context("Schlüsselableitung fehlgeschlagen")?;
info!("Entschlüssele DEK via AES-256-GCM ..."); ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM...");
let dek = unwrap_dek( let dek = unwrap_dek(
&kek, &kek,
&meta.wrapped_dek, &meta.wrapped_dek,
@@ -124,10 +118,6 @@ pub async fn mount_container(
let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await { let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await {
Ok(l) => l, Ok(l) => l,
Err(_) if requested_port.is_none() => { 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))) TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
.await .await
.context("Konnte keinen lokalen TCP-Port binden")? .context("Konnte keinen lokalen TCP-Port binden")?
@@ -139,7 +129,16 @@ pub async fn mount_container(
let bound_addr = listener.local_addr()?; let bound_addr = listener.local_addr()?;
let bound_port = bound_addr.port(); let bound_port = bound_addr.port();
info!("WebDAV-Server lauscht auf http://{}", bound_addr);
ui::step(
4,
4,
"🌐",
&format!(
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
bound_port, drive_str
),
);
let (shutdown_tx, mut shutdown_rx) = watch::channel(false); let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
@@ -175,7 +174,7 @@ pub async fn mount_container(
}); });
} }
_ = shutdown_rx.changed() => { _ = shutdown_rx.changed() => {
info!("WebDAV-Server-Task empfängt Shutdown-Signal."); debug!("WebDAV-Server-Task empfängt Shutdown-Signal.");
break; break;
} }
} }
@@ -190,13 +189,15 @@ pub async fn mount_container(
} }
println!(); println!();
println!("============================================================"); println!("┌─────────────────────────────────────────────────────────────┐");
println!(" Sanctum Container erfolgreich gemountet!"); println!("│ ✔ Sanctum Container erfolgreich gemountet");
println!(" Pfad: {}", container_path.display()); println!("└─────────────────────────────────────────────────────────────┘");
println!(" Laufwerk: {}", drive_str); println!();
println!(" WebDAV URL: http://127.0.0.1:{}/", bound_port); println!(" • Container: {}", container_path.display());
println!(" Drücke [Ctrl+C] zum sauberen Trennen und Schließen."); println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
println!("============================================================"); println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
println!();
println!(" [{}] Drücke [Ctrl+C] zum sicheren Trennen und Schließen.", ui::yellow("Tipp"));
println!(); println!();
// Warten auf Strg+C // Warten auf Strg+C
@@ -205,12 +206,15 @@ pub async fn mount_container(
.context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?; .context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?;
println!(); println!();
info!("Beendigungssignal (Ctrl+C) erhalten."); println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
info!("Trennen des Windows-Netzlaufwerks {} ...", drive_str); print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
let _ = std::io::Write::flush(&mut std::io::stdout());
// Automatisches Unmount // Automatisches Unmount
if let Err(e) = unmount_drive(drive_letter) { if let Err(e) = unmount_drive(drive_letter) {
warn!("Warnung beim automatischen Unmount: {e}"); println!("{}", ui::yellow(&format!("Warnung ({e})")));
} else {
println!("{}", ui::green("OK"));
} }
// HTTP Server beenden // HTTP Server beenden
@@ -218,11 +222,17 @@ pub async fn mount_container(
let _ = server_handle.await; let _ = server_handle.await;
// SQLite WAL Checkpoint erzwingen // SQLite WAL Checkpoint erzwingen
info!("Führe SQLite WAL-Checkpoint aus (PRAGMA wal_checkpoint(TRUNCATE)) ..."); print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
let _ = std::io::Write::flush(&mut std::io::stdout());
if let Err(e) = db.checkpoint() { if let Err(e) = db.checkpoint() {
error!("Fehler beim WAL-Checkpoint: {e}"); println!("{}", ui::red(&format!("Fehler ({e})")));
} else {
println!("{}", ui::green("OK"));
} }
info!("Sanctum Container wurde sicher und vollständig geschlossen."); println!();
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green(""));
println!();
Ok(()) Ok(())
} }
+99
View File
@@ -0,0 +1,99 @@
use std::sync::atomic::{AtomicBool, Ordering};
static VT_ENABLED: AtomicBool = AtomicBool::new(false);
/// Initialisiert die Windows-Konsole:
/// - Setzt Code-Page 65001 (UTF-8) für Ein- und Ausgabe (verhindert Umlaut-Fehler wie `f├╝r`)
/// - Aktiviert VIRTUAL_TERMINAL_PROCESSING für saubere ANSI-Farb- und Escape-Sequenz-Darstellung
#[cfg(windows)]
pub fn init_console() {
const CP_UTF8: u32 = 65001;
const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5; // (DWORD)-11
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
extern "system" {
fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
fn SetConsoleCP(wCodePageID: u32) -> i32;
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
fn GetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, lpMode: *mut u32) -> i32;
fn SetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, dwMode: u32) -> i32;
}
unsafe {
let _ = SetConsoleOutputCP(CP_UTF8);
let _ = SetConsoleCP(CP_UTF8);
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
if !handle.is_null() && handle as isize != -1 {
let mut mode: u32 = 0;
if GetConsoleMode(handle, &mut mode) != 0 {
if SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 {
VT_ENABLED.store(true, Ordering::Relaxed);
}
}
}
}
}
#[cfg(not(windows))]
pub fn init_console() {
VT_ENABLED.store(true, Ordering::Relaxed);
}
pub fn is_vt_enabled() -> bool {
VT_ENABLED.load(Ordering::Relaxed)
}
pub fn bold(s: &str) -> String {
if is_vt_enabled() {
format!("\x1b[1m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn dim(s: &str) -> String {
if is_vt_enabled() {
format!("\x1b[90m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn green(s: &str) -> String {
if is_vt_enabled() {
format!("\x1b[1;32m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn cyan(s: &str) -> String {
if is_vt_enabled() {
format!("\x1b[1;36m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn yellow(s: &str) -> String {
if is_vt_enabled() {
format!("\x1b[1;33m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn red(s: &str) -> String {
if is_vt_enabled() {
format!("\x1b[1;31m{s}\x1b[0m")
} else {
s.to_string()
}
}
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
let tag = cyan(&format!("[{}/{}]", num, total));
println!(" {} {} {}", tag, icon, msg);
}