feat(opsec): implement inactivity auto-lock, session lock detection, and anti-leak shield
This commit is contained in:
+232
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user