Files
sanctum/src/windows.rs
T

434 lines
13 KiB
Rust

use std::process::Command;
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 direkt im Windows Explorer.
pub fn open_in_explorer(drive_char: char) -> Result<()> {
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(())
}
/// 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(),
);
}
}
}
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte).
pub fn register_explorer_integration() -> Result<()> {
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",
"",
"Als Laufwerk einbinden",
),
(
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
"",
&format!("\"{exe_str}\" mount --path \"%1\""),
),
// 5. Kontextmenü-Aktion: Integritätsprüfung
(
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
"",
"Integrität prüfen (FSCK)",
),
(
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
"",
&format!("\"{exe_str}\" verify --path \"%1\""),
),
// 6. Kontextmenü-Aktion: Header sichern
(
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header",
"",
"Header sichern",
),
(
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command",
"",
&format!("\"{exe_str}\" backup-header --path \"%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(())
}
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU).
pub fn unregister_explorer_integration() -> Result<()> {
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(())
}
/// 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;
}
// 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.
pub struct SessionLockGuard {
#[cfg(windows)]
hwnd: isize,
#[cfg(windows)]
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
}