feat(opsec): implement inactivity auto-lock, session lock detection, and anti-leak shield

This commit is contained in:
2026-09-08 10:14:31 +02:00
parent 38df3d3845
commit 2cb065c8c0
5 changed files with 727 additions and 7 deletions
+232
View File
@@ -188,6 +188,229 @@ pub fn get_default_system_icon() -> Option<tray_item::IconSource> {
}
}
/// 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::*;
@@ -198,4 +421,13 @@ mod tests {
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);
}
}