use std::process::Command; #[allow(unused_imports)] use anyhow::{bail, Context, Result}; /// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D'). pub fn find_next_available_drive() -> Result { #[cfg(windows)] { extern "system" { fn GetLogicalDrives() -> u32; } let mask = unsafe { GetLogicalDrives() }; if mask == 0 { bail!("Fehler beim Abfragen der logischen Laufwerke via Win32 API"); } // Suche von 'Z' abwärts bis 'D' (A, B für Diskettenlaufwerke, C für System reservieren) for ch in ('D'..='Z').rev() { let bit_index = (ch as u8) - b'A'; if (mask & (1 << bit_index)) == 0 { return Ok(ch); } } bail!("Kein freier Windows-Laufwerksbuchstabe (D: bis Z:) verfügbar!"); } #[cfg(not(windows))] { Ok('S') } } /// Öffnet das eingebundene Netzlaufwerk oder Verzeichnis direkt im systemeigenen Dateimanager /// (Windows: Explorer, macOS: open, Linux: xdg-open). pub fn open_in_explorer(drive_char: char) -> Result<()> { #[cfg(windows)] { let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase()); Command::new("explorer.exe") .arg(&drive_path) .spawn() .with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?; Ok(()) } #[cfg(target_os = "macos")] { let _ = drive_char; let _ = Command::new("open").arg(".").spawn(); Ok(()) } #[cfg(all(unix, not(target_os = "macos")))] { let _ = drive_char; let _ = Command::new("xdg-open").arg(".").spawn(); Ok(()) } #[cfg(not(any(windows, unix)))] { let _ = drive_char; Ok(()) } } /// Öffnet einen beliebigen Pfad im nativen Dateimanager der Plattform. pub fn open_in_file_manager(path: &std::path::Path) -> Result<()> { #[cfg(windows)] { Command::new("explorer.exe") .arg(path) .spawn() .with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", path.display()))?; Ok(()) } #[cfg(target_os = "macos")] { Command::new("open") .arg(path) .spawn() .with_context(|| format!("Konnte macOS Finder für '{}' nicht öffnen", path.display()))?; Ok(()) } #[cfg(all(unix, not(target_os = "macos")))] { Command::new("xdg-open") .arg(path) .spawn() .with_context(|| format!("Konnte Dateimanager via xdg-open für '{}' nicht öffnen", path.display()))?; Ok(()) } #[cfg(not(any(windows, unix)))] { let _ = path; Ok(()) } } /// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED). pub fn notify_shell_associations_changed() { #[cfg(windows)] { extern "system" { fn SHChangeNotify( w_event_id: i32, u_flags: u32, dw_item1: *const std::ffi::c_void, dw_item2: *const std::ffi::c_void, ); } const SHCNE_ASSOCCHANGED: i32 = 0x0800_0000; const SHCNF_IDLIST: u32 = 0x0000; unsafe { SHChangeNotify( SHCNE_ASSOCCHANGED, SHCNF_IDLIST, std::ptr::null(), std::ptr::null(), ); } } #[cfg(all(unix, not(target_os = "macos")))] { let _ = Command::new("update-desktop-database").spawn(); } #[cfg(not(any(windows, all(unix, not(target_os = "macos")))))] { } } /// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer bzw. unter Linux via Freedesktop. pub fn register_explorer_integration() -> Result<()> { #[cfg(windows)] { let current_exe = std::env::current_exe() .context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?; let exe_str = current_exe.display().to_string(); let reg_commands = [ // 1. .sanctum Erweiterung mit ProgID verknüpfen ( r"HKCU\Software\Classes\.sanctum", "", "Sanctum.Container", ), // 2. ProgID Metadaten & Beschreibung ( r"HKCU\Software\Classes\Sanctum.Container", "", "Sanctum Verschlüsselter Container", ), // 3. Icon ( r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon", "", &format!("\"{exe_str}\",0"), ), // 4. Standard-Doppelklick-Aktion: Mount ( r"HKCU\Software\Classes\Sanctum.Container\shell\open", "", "In Sanctum öffnen", ), ( r"HKCU\Software\Classes\Sanctum.Container\shell\open\command", "", &format!("\"{exe_str}\" mount \"%1\""), ), // 5. Kontextmenü-Aktion: Verify / FSCK ( r"HKCU\Software\Classes\Sanctum.Container\shell\verify", "", "Integrität prüfen (FSCK)", ), ( r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command", "", &format!("cmd /k \"\"{exe_str}\" verify \"%1\"\""), ), // 6. Kontextmenü-Aktion: Header-Backup ( r"HKCU\Software\Classes\Sanctum.Container\shell\backup", "", "Header sichern (Disaster Recovery)", ), ( r"HKCU\Software\Classes\Sanctum.Container\shell\backup\command", "", &format!("cmd /k \"\"{exe_str}\" backup-header \"%1\"\""), ), ]; for (key, val_name, val_data) in reg_commands { let mut cmd = Command::new("reg"); cmd.arg("add").arg(key); if val_name.is_empty() { cmd.arg("/ve"); } else { cmd.arg("/v").arg(val_name); } cmd.arg("/d").arg(val_data).arg("/f"); let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!("Registry-Fehler beim Anlegen von {key}: {stderr}"); } } notify_shell_associations_changed(); Ok(()) } #[cfg(not(windows))] { if let Some(home) = std::env::var_os("HOME") { let home_path = std::path::PathBuf::from(home); let apps_dir = home_path.join(".local/share/applications"); let mime_dir = home_path.join(".local/share/mime/packages"); let _ = std::fs::create_dir_all(&apps_dir); let _ = std::fs::create_dir_all(&mime_dir); let desktop_content = "[Desktop Entry]\nType=Application\nName=Sanctum\nComment=Verschlüsselter Ein-Datei-Container\nExec=sanctum mount %f\nIcon=security-high\nTerminal=true\nMimeType=application/x-sanctum;\nCategories=Utility;Security;\n"; let _ = std::fs::write(apps_dir.join("sanctum.desktop"), desktop_content); let mime_content = "\n\n \n Sanctum Verschlüsselter Container\n \n \n\n"; let _ = std::fs::write(mime_dir.join("application-x-sanctum.xml"), mime_content); notify_shell_associations_changed(); } Ok(()) } } /// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU) bzw. unter Linux aus Freedesktop. pub fn unregister_explorer_integration() -> Result<()> { #[cfg(windows)] { let keys_to_delete = [ r"HKCU\Software\Classes\.sanctum", r"HKCU\Software\Classes\Sanctum.Container", ]; for key in keys_to_delete { let _ = Command::new("reg") .args(["delete", key, "/f"]) .output(); } notify_shell_associations_changed(); Ok(()) } #[cfg(not(windows))] { if let Some(home) = std::env::var_os("HOME") { let home_path = std::path::PathBuf::from(home); let _ = std::fs::remove_file(home_path.join(".local/share/applications/sanctum.desktop")); let _ = std::fs::remove_file(home_path.join(".local/share/mime/packages/application-x-sanctum.xml")); notify_shell_associations_changed(); } Ok(()) } } /// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray. #[cfg(windows)] pub fn get_default_system_icon() -> Option { extern "system" { fn LoadIconW(instance: isize, icon_name: *const u16) -> isize; fn GetModuleHandleW(module_name: *const u16) -> isize; } // 1. Eingebettetes Anwendungs-Icon (Ressource ID 1) aus eigenem Modul laden let h_instance = unsafe { GetModuleHandleW(std::ptr::null()) }; let app_icon = unsafe { LoadIconW(h_instance, 1 as *const u16) }; if app_icon != 0 { return Some(tray_item::IconSource::RawIcon(app_icon)); } // 2. Fallback: Windows IDI_SHIELD = 32518, IDI_APPLICATION = 32512 let icon = unsafe { LoadIconW(0, 32518 as *const u16) }; if icon != 0 { Some(tray_item::IconSource::RawIcon(icon)) } else { let icon_app = unsafe { LoadIconW(0, 32512 as *const u16) }; if icon_app != 0 { Some(tray_item::IconSource::RawIcon(icon_app)) } else { None } } } /// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre. /// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet. #[cfg(windows)] pub struct SessionLockGuard { hwnd: isize, join_handle: Option>, } #[cfg(windows)] impl Drop for SessionLockGuard { fn drop(&mut self) { if self.hwnd != 0 { unsafe { PostMessageW(self.hwnd, 0x0010 /* WM_CLOSE */, 0, 0); } } if let Ok(mut guard) = SESSION_LOCK_TX.lock() { *guard = None; } if let Some(handle) = self.join_handle.take() { let _ = handle.join(); } } } #[cfg(not(windows))] pub struct SessionLockGuard; #[cfg(windows)] static SESSION_LOCK_TX: std::sync::Mutex>> = std::sync::Mutex::new(None); #[cfg(windows)] #[allow(non_snake_case)] #[repr(C)] struct WNDCLASSEXW { cbSize: u32, style: u32, lpfnWndProc: Option isize>, cbClsExtra: i32, cbWndExtra: i32, hInstance: isize, hIcon: isize, hCursor: isize, hbrBackground: isize, lpszMenuName: *const u16, lpszClassName: *const u16, hIconSm: isize, } #[cfg(windows)] #[allow(non_snake_case)] #[repr(C)] struct MSG { hwnd: isize, message: u32, wParam: usize, lParam: isize, time: u32, pt_x: i32, pt_y: i32, } #[cfg(windows)] extern "system" { fn GetModuleHandleW(lpModuleName: *const u16) -> isize; fn RegisterClassExW(lpwcx: *const WNDCLASSEXW) -> u16; fn UnregisterClassW(lpClassName: *const u16, hInstance: isize) -> i32; fn CreateWindowExW( dwExStyle: u32, lpClassName: *const u16, lpWindowName: *const u16, dwStyle: u32, x: i32, y: i32, nWidth: i32, nHeight: i32, hWndParent: isize, hMenu: isize, hInstance: isize, lpParam: *mut std::ffi::c_void, ) -> isize; fn DestroyWindow(hwnd: isize) -> i32; fn DefWindowProcW(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> isize; fn GetMessageW(lpMsg: *mut MSG, hWnd: isize, wMsgFilterMin: u32, wMsgFilterMax: u32) -> i32; fn TranslateMessage(lpMsg: *const MSG) -> i32; fn DispatchMessageW(lpMsg: *const MSG) -> isize; fn PostMessageW(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> i32; fn PostQuitMessage(nExitCode: i32); } #[cfg(windows)] #[link(name = "wtsapi32")] extern "system" { fn WTSRegisterSessionNotification(hwnd: isize, flags: u32) -> i32; fn WTSUnRegisterSessionNotification(hwnd: isize) -> i32; } #[cfg(windows)] unsafe extern "system" fn session_wnd_proc( hwnd: isize, msg: u32, wparam: usize, lparam: isize, ) -> isize { const WM_CLOSE: u32 = 0x0010; const WM_WTSSESSION_CHANGE: u32 = 0x02B1; const WTS_SESSION_LOCK: usize = 0x7; const WTS_SESSION_LOGOFF: usize = 0x6; match msg { WM_WTSSESSION_CHANGE => { if wparam == WTS_SESSION_LOCK || wparam == WTS_SESSION_LOGOFF { if let Ok(guard) = SESSION_LOCK_TX.lock() { if let Some(ref tx) = *guard { let _ = tx.blocking_send(()); } } } 0 } WM_CLOSE => { PostQuitMessage(0); 0 } _ => DefWindowProcW(hwnd, msg, wparam, lparam), } } /// Startet einen Hintergrundthread mit einem verdeckten Win32-Nachrichtenfenster (HWND_MESSAGE), /// das via `WTSRegisterSessionNotification` auf Sperr-Events (Win + L) lauscht und beim Eintreffen /// ein Signal an den übergebenen Tokio-Kanal sendet. #[cfg(windows)] pub fn start_session_lock_monitor( shutdown_tx: tokio::sync::mpsc::Sender<()>, ) -> Result { if let Ok(mut guard) = SESSION_LOCK_TX.lock() { *guard = Some(shutdown_tx); } let (hwnd_tx, hwnd_rx) = std::sync::mpsc::channel::(); let join_handle = std::thread::Builder::new() .name("sanctum-session-monitor".to_string()) .spawn(move || unsafe { let h_instance = GetModuleHandleW(std::ptr::null()); let class_name: Vec = "SanctumSessionMonitorClass\0".encode_utf16().collect(); let wcx = WNDCLASSEXW { cbSize: std::mem::size_of::() as u32, style: 0, lpfnWndProc: Some(session_wnd_proc), cbClsExtra: 0, cbWndExtra: 0, hInstance: h_instance, hIcon: 0, hCursor: 0, hbrBackground: 0, lpszMenuName: std::ptr::null(), lpszClassName: class_name.as_ptr(), hIconSm: 0, }; RegisterClassExW(&wcx); let hwnd = CreateWindowExW( 0, class_name.as_ptr(), std::ptr::null(), 0, 0, 0, 0, 0, -3, // HWND_MESSAGE 0, h_instance, std::ptr::null_mut(), ); if hwnd == 0 { let _ = hwnd_tx.send(0); return; } WTSRegisterSessionNotification(hwnd, 0); // NOTIFY_FOR_THIS_SESSION = 0 let _ = hwnd_tx.send(hwnd); let mut msg = std::mem::zeroed::(); while GetMessageW(&mut msg, 0, 0, 0) > 0 { TranslateMessage(&msg); DispatchMessageW(&msg); } WTSUnRegisterSessionNotification(hwnd); DestroyWindow(hwnd); UnregisterClassW(class_name.as_ptr(), h_instance); }) .context("Konnte Windows Session-Monitor-Thread nicht starten")?; let hwnd = hwnd_rx .recv() .map_err(|e| anyhow::anyhow!("Session-Monitor-Thread initialisierte nicht rechtzeitig: {e}"))?; if hwnd == 0 { bail!("Win32-Nachrichtenfenster für Session-Lock konnte nicht erstellt werden"); } Ok(SessionLockGuard { hwnd, join_handle: Some(join_handle), }) } #[cfg(not(windows))] pub fn start_session_lock_monitor( _shutdown_tx: tokio::sync::mpsc::Sender<()>, ) -> Result { Ok(SessionLockGuard) } // ───────────────────────────────────────────────────────────── // Console Close Event Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT) // ───────────────────────────────────────────────────────────── #[cfg(windows)] static CONSOLE_CTRL_TX: std::sync::Mutex>> = std::sync::Mutex::new(None); #[cfg(windows)] static CONSOLE_CTRL_DRIVE: std::sync::Mutex> = std::sync::Mutex::new(None); #[cfg(windows)] unsafe extern "system" fn console_ctrl_routine(ctrl_type: u32) -> i32 { const CTRL_C_EVENT: u32 = 0; const CTRL_BREAK_EVENT: u32 = 1; const CTRL_CLOSE_EVENT: u32 = 2; const CTRL_LOGOFF_EVENT: u32 = 5; const CTRL_SHUTDOWN_EVENT: u32 = 6; match ctrl_type { CTRL_CLOSE_EVENT | CTRL_LOGOFF_EVENT | CTRL_SHUTDOWN_EVENT => { // 1. Sofortiges Notfall-Unmount direkt aus dem Win32-Callback ausführen if let Ok(guard) = CONSOLE_CTRL_DRIVE.lock() { if let Some(dl) = *guard { let drive_str = format!("{}:", dl.to_ascii_uppercase()); let _ = std::process::Command::new("net") .args(["use", &drive_str, "/delete", "/y"]) .output(); } } // 2. Asynchronen Shutdown-Kanal benachrichtigen (für SQLite WAL Checkpoint) if let Ok(guard) = CONSOLE_CTRL_TX.lock() { if let Some(ref tx) = *guard { let _ = tx.blocking_send(()); } } 1 // TRUE: Event abgearbeitet } CTRL_C_EVENT | CTRL_BREAK_EVENT => { // Ctrl+C wird primär von tokio::signal::ctrl_c() behandelt 0 } _ => 0, } } /// RAII Guard zur sauberen Deregistrierung des Win32 Console-Control-Handlers. pub struct ConsoleCtrlGuard; impl Drop for ConsoleCtrlGuard { fn drop(&mut self) { #[cfg(windows)] { extern "system" { fn SetConsoleCtrlHandler( handler: Option i32>, add: i32, ) -> i32; } unsafe { SetConsoleCtrlHandler(Some(console_ctrl_routine), 0); } if let Ok(mut guard) = CONSOLE_CTRL_TX.lock() { *guard = None; } if let Ok(mut guard) = CONSOLE_CTRL_DRIVE.lock() { *guard = None; } } } } /// Registriert einen Win32 Console Control Handler für CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT /// und CTRL_SHUTDOWN_EVENT, um verwaiste Netzlaufwerke beim Schließen des Konsolenfensters zu verhindern. pub fn start_console_ctrl_monitor( shutdown_tx: tokio::sync::mpsc::Sender<()>, drive_letter: char, ) -> Result { #[cfg(windows)] { extern "system" { fn SetConsoleCtrlHandler( handler: Option i32>, add: i32, ) -> i32; } if let Ok(mut guard) = CONSOLE_CTRL_TX.lock() { *guard = Some(shutdown_tx); } if let Ok(mut guard) = CONSOLE_CTRL_DRIVE.lock() { *guard = Some(drive_letter); } let res = unsafe { SetConsoleCtrlHandler(Some(console_ctrl_routine), 1) }; if res == 0 { bail!("Konnte Win32 SetConsoleCtrlHandler nicht registrieren"); } Ok(ConsoleCtrlGuard) } #[cfg(not(windows))] { let _ = shutdown_tx; let _ = drive_letter; Ok(ConsoleCtrlGuard) } } /// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys unter Windows bzw. Swap unter Linux/macOS). pub fn lock_memory(ptr: *const u8, len: usize) -> bool { #[cfg(windows)] { extern "system" { fn VirtualLock(lpaddress: *const std::ffi::c_void, dwsize: usize) -> i32; } if ptr.is_null() || len == 0 { return false; } unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 } } #[cfg(unix)] { extern "C" { fn mlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int; } if ptr.is_null() || len == 0 { return false; } unsafe { mlock(ptr as *const std::ffi::c_void, len) == 0 } } #[cfg(not(any(windows, unix)))] { let _ = (ptr, len); false } } /// Entriegelt einen zuvor mit `lock_memory` geschützten Speicherbereich im RAM. pub fn unlock_memory(ptr: *const u8, len: usize) -> bool { #[cfg(windows)] { extern "system" { fn VirtualUnlock(lpaddress: *const std::ffi::c_void, dwsize: usize) -> i32; } if ptr.is_null() || len == 0 { return false; } unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 } } #[cfg(unix)] { extern "C" { fn munlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int; } if ptr.is_null() || len == 0 { return false; } unsafe { munlock(ptr as *const std::ffi::c_void, len) == 0 } } #[cfg(not(any(windows, unix)))] { let _ = (ptr, len); false } } #[cfg(test)] mod tests { use super::*; #[test] fn test_virtual_lock_memory_lifecycle() { let buffer = vec![0x42u8; 4096]; let _ = lock_memory(buffer.as_ptr(), buffer.len()); let _ = unlock_memory(buffer.as_ptr(), buffer.len()); assert_eq!(buffer[0], 0x42); } #[test] fn test_find_next_available_drive() { let drive = find_next_available_drive().expect("Find next drive"); assert!(drive.is_ascii_alphabetic()); assert!(drive >= 'D' && drive <= 'Z'); } #[test] fn test_session_lock_monitor_lifecycle() { let (tx, _rx) = tokio::sync::mpsc::channel(1); let monitor = start_session_lock_monitor(tx); assert!(monitor.is_ok()); // Dropping monitor closes message loop and joins thread cleanly drop(monitor); } #[test] fn test_console_ctrl_monitor_lifecycle() { let (tx, _rx) = tokio::sync::mpsc::channel(1); let monitor = start_console_ctrl_monitor(tx, 'Z'); assert!(monitor.is_ok()); drop(monitor); } }