Files
sanctum/src/windows.rs
T
harald 4ef0c414fa
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
feat(linux): add static musl x86_64 build, packaging, and Freedesktop integration
2026-09-10 17:45:04 +02:00

721 lines
23 KiB
Rust

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<char> {
#[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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n <mime-type type=\"application/x-sanctum\">\n <comment>Sanctum Verschlüsselter Container</comment>\n <glob pattern=\"*.sanctum\"/>\n </mime-type>\n</mime-info>\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<tray_item::IconSource> {
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<std::thread::JoinHandle<()>>,
}
#[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<Option<tokio::sync::mpsc::Sender<()>>> =
std::sync::Mutex::new(None);
#[cfg(windows)]
#[allow(non_snake_case)]
#[repr(C)]
struct WNDCLASSEXW {
cbSize: u32,
style: u32,
lpfnWndProc: Option<unsafe extern "system" fn(isize, u32, usize, isize) -> 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<SessionLockGuard> {
if let Ok(mut guard) = SESSION_LOCK_TX.lock() {
*guard = Some(shutdown_tx);
}
let (hwnd_tx, hwnd_rx) = std::sync::mpsc::channel::<isize>();
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<u16> = "SanctumSessionMonitorClass\0".encode_utf16().collect();
let wcx = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() 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::<MSG>();
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<SessionLockGuard> {
Ok(SessionLockGuard)
}
// ─────────────────────────────────────────────────────────────
// Console Close Event Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT)
// ─────────────────────────────────────────────────────────────
#[cfg(windows)]
static CONSOLE_CTRL_TX: std::sync::Mutex<Option<tokio::sync::mpsc::Sender<()>>> = std::sync::Mutex::new(None);
#[cfg(windows)]
static CONSOLE_CTRL_DRIVE: std::sync::Mutex<Option<char>> = 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<unsafe extern "system" fn(u32) -> 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<ConsoleCtrlGuard> {
#[cfg(windows)]
{
extern "system" {
fn SetConsoleCtrlHandler(
handler: Option<unsafe extern "system" fn(u32) -> 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);
}
}