feat(opsec): implement inactivity auto-lock, session lock detection, and anti-leak shield
This commit is contained in:
+29
-1
@@ -60,6 +60,18 @@ enum Commands {
|
|||||||
/// Deaktiviert das Windows System-Tray Icon während des Mounts
|
/// Deaktiviert das Windows System-Tray Icon während des Mounts
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_tray: bool,
|
no_tray: bool,
|
||||||
|
|
||||||
|
/// Automatisches Aushängen und Sperren nach N Sekunden Inaktivität (z. B. 300 für 5 Minuten)
|
||||||
|
#[arg(long, value_name = "SECS")]
|
||||||
|
idle_timeout: Option<u64>,
|
||||||
|
|
||||||
|
/// Verhindert das automatische Sperren beim Sperren des Windows-Bildschirms (Win + L)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
no_screen_lock: bool,
|
||||||
|
|
||||||
|
/// Deaktiviert das Blockieren und Verbergen von Windows Explorer Metadaten (Thumbs.db, desktop.ini)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
no_anti_leak: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||||
@@ -481,6 +493,9 @@ async fn run() -> Result<()> {
|
|||||||
recovery_key,
|
recovery_key,
|
||||||
no_open,
|
no_open,
|
||||||
no_tray,
|
no_tray,
|
||||||
|
idle_timeout,
|
||||||
|
no_screen_lock,
|
||||||
|
no_anti_leak,
|
||||||
} => {
|
} => {
|
||||||
let drive_char = match drive {
|
let drive_char = match drive {
|
||||||
Some(ref d) => parse_drive_letter(d)?,
|
Some(ref d) => parse_drive_letter(d)?,
|
||||||
@@ -508,7 +523,20 @@ async fn run() -> Result<()> {
|
|||||||
|
|
||||||
let open_explorer = !no_open;
|
let open_explorer = !no_open;
|
||||||
let enable_tray = !no_tray;
|
let enable_tray = !no_tray;
|
||||||
mount_container(&path, drive_char, port, auth, open_explorer, enable_tray).await?;
|
let lock_on_screen_lock = !no_screen_lock;
|
||||||
|
let anti_leak = !no_anti_leak;
|
||||||
|
mount_container(
|
||||||
|
&path,
|
||||||
|
drive_char,
|
||||||
|
port,
|
||||||
|
auth,
|
||||||
|
open_explorer,
|
||||||
|
enable_tray,
|
||||||
|
idle_timeout,
|
||||||
|
lock_on_screen_lock,
|
||||||
|
anti_leak,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Commands::Unmount { drive } => {
|
Commands::Unmount { drive } => {
|
||||||
let drive_char = parse_drive_letter(&drive)?;
|
let drive_char = parse_drive_letter(&drive)?;
|
||||||
|
|||||||
+64
-3
@@ -2,6 +2,8 @@ use std::convert::Infallible;
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use dav_server::{fakels::FakeLs, DavHandler};
|
use dav_server::{fakels::FakeLs, DavHandler};
|
||||||
@@ -83,6 +85,9 @@ pub async fn mount_container(
|
|||||||
auth: ContainerAuth,
|
auth: ContainerAuth,
|
||||||
open_explorer: bool,
|
open_explorer: bool,
|
||||||
enable_tray: bool,
|
enable_tray: bool,
|
||||||
|
idle_timeout: Option<u64>,
|
||||||
|
lock_on_screen_lock: bool,
|
||||||
|
anti_leak: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
|
|
||||||
@@ -127,8 +132,9 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// WebDAV Filesystem und Handler konfigurieren
|
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield)
|
||||||
let fs = SanctumFs::new(db.clone(), dek, version);
|
let fs = SanctumFs::with_options(db.clone(), dek, version, anti_leak);
|
||||||
|
let last_activity = fs.last_activity();
|
||||||
let dav_server = DavHandler::builder()
|
let dav_server = DavHandler::builder()
|
||||||
.filesystem(Box::new(fs))
|
.filesystem(Box::new(fs))
|
||||||
.locksystem(FakeLs::new())
|
.locksystem(FakeLs::new())
|
||||||
@@ -248,6 +254,44 @@ pub async fn mount_container(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Inaktivitäts-Timer (Auto-Lock)
|
||||||
|
let (idle_shutdown_tx, mut idle_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
|
if let Some(timeout_secs) = idle_timeout {
|
||||||
|
if timeout_secs > 0 {
|
||||||
|
let last_act = last_activity.clone();
|
||||||
|
let idle_tx = idle_shutdown_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let last = last_act.load(Ordering::Relaxed);
|
||||||
|
if now.saturating_sub(last) >= timeout_secs {
|
||||||
|
let _ = idle_tx.send(()).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows-Sitzungssperre (Win + L Auto-Lock)
|
||||||
|
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
|
let _session_monitor = if lock_on_screen_lock {
|
||||||
|
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
||||||
|
Ok(guard) => Some(guard),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Konnte Windows Session-Lock Monitor nicht aktivieren: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
||||||
@@ -256,6 +300,15 @@ pub async fn mount_container(
|
|||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
||||||
|
if let Some(secs) = idle_timeout {
|
||||||
|
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
||||||
|
}
|
||||||
|
if lock_on_screen_lock {
|
||||||
|
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
|
||||||
|
}
|
||||||
|
if anti_leak {
|
||||||
|
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
|
||||||
|
}
|
||||||
if enable_tray {
|
if enable_tray {
|
||||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
||||||
}
|
}
|
||||||
@@ -263,7 +316,7 @@ pub async fn mount_container(
|
|||||||
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Warten auf Strg+C ODER Signal aus dem System-Tray
|
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L)
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = tokio::signal::ctrl_c() => {
|
res = tokio::signal::ctrl_c() => {
|
||||||
let _ = res;
|
let _ = res;
|
||||||
@@ -274,6 +327,14 @@ pub async fn mount_container(
|
|||||||
println!();
|
println!();
|
||||||
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
||||||
}
|
}
|
||||||
|
_ = session_lock_rx.recv() => {
|
||||||
|
println!();
|
||||||
|
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
_ = idle_shutdown_rx.recv() => {
|
||||||
|
println!();
|
||||||
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
|||||||
+290
-3
@@ -1,5 +1,6 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io::SeekFrom;
|
use std::io::SeekFrom;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -18,6 +19,31 @@ use zeroize::Zeroizing;
|
|||||||
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
||||||
use crate::storage::{Database, NodeRecord};
|
use crate::storage::{Database, NodeRecord};
|
||||||
|
|
||||||
|
/// Prüft, ob ein Dateiname zu den typischen Windows Explorer Metadaten-, Cache-
|
||||||
|
/// oder Thumbnail-Dateien gehört (z. B. Thumbs.db, desktop.ini), die standardmäßig
|
||||||
|
/// im Container blockiert und verborgen werden (Anti-Leak Shield).
|
||||||
|
pub fn is_leak_file(filename: &str) -> bool {
|
||||||
|
let lower = filename.trim().to_ascii_lowercase();
|
||||||
|
match lower.as_str() {
|
||||||
|
"thumbs.db"
|
||||||
|
| "ehthumbs.db"
|
||||||
|
| "ehthumbs_vista.db"
|
||||||
|
| "desktop.ini"
|
||||||
|
| "folder.jpg"
|
||||||
|
| "albumartsmall.jpg"
|
||||||
|
| "autorun.inf"
|
||||||
|
| ".ds_store" => true,
|
||||||
|
_ => {
|
||||||
|
if lower.starts_with("albumart") && (lower.ends_with(".jpg") || lower.ends_with(".ini"))
|
||||||
|
{
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Metadaten
|
// Metadaten
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -87,6 +113,7 @@ pub struct SanctumFile {
|
|||||||
// (chunk_index, decrypted_payload, is_dirty)
|
// (chunk_index, decrypted_payload, is_dirty)
|
||||||
cached_chunk: Option<(u32, Vec<u8>, bool)>,
|
cached_chunk: Option<(u32, Vec<u8>, bool)>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
|
last_activity: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for SanctumFile {
|
impl Debug for SanctumFile {
|
||||||
@@ -106,6 +133,7 @@ impl SanctumFile {
|
|||||||
db: Database,
|
db: Database,
|
||||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
|
last_activity: Arc<AtomicU64>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let meta = SanctumMetaData {
|
let meta = SanctumMetaData {
|
||||||
is_dir: node.is_dir,
|
is_dir: node.is_dir,
|
||||||
@@ -123,9 +151,18 @@ impl SanctumFile {
|
|||||||
meta,
|
meta,
|
||||||
cached_chunk: None,
|
cached_chunk: None,
|
||||||
format_version,
|
format_version,
|
||||||
|
last_activity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn touch(&self) {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.last_activity.store(now, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Schreibt den aktuell im RAM gehaltenen Chunk verschlüsselt in die SQLite-Datenbank zurück.
|
/// Schreibt den aktuell im RAM gehaltenen Chunk verschlüsselt in die SQLite-Datenbank zurück.
|
||||||
fn flush_cached_chunk(&mut self) -> Result<(), FsError> {
|
fn flush_cached_chunk(&mut self) -> Result<(), FsError> {
|
||||||
@@ -211,6 +248,7 @@ impl DavFile for SanctumFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_bytes(&mut self, mut count: usize) -> FsFuture<'_, Bytes> {
|
fn read_bytes(&mut self, mut count: usize) -> FsFuture<'_, Bytes> {
|
||||||
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if self.cursor >= self.file_size || count == 0 {
|
if self.cursor >= self.file_size || count == 0 {
|
||||||
return Ok(Bytes::new());
|
return Ok(Bytes::new());
|
||||||
@@ -253,6 +291,7 @@ impl DavFile for SanctumFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
|
||||||
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut src = &buf[..];
|
let mut src = &buf[..];
|
||||||
|
|
||||||
@@ -301,6 +340,7 @@ impl DavFile for SanctumFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
|
fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
|
||||||
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let new_cursor = match pos {
|
let new_cursor = match pos {
|
||||||
SeekFrom::Start(offset) => offset as i64,
|
SeekFrom::Start(offset) => offset as i64,
|
||||||
@@ -318,6 +358,7 @@ impl DavFile for SanctumFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> FsFuture<'_, ()> {
|
fn flush(&mut self) -> FsFuture<'_, ()> {
|
||||||
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.flush_cached_chunk()?;
|
self.flush_cached_chunk()?;
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
@@ -346,17 +387,50 @@ pub struct SanctumFs {
|
|||||||
db: Database,
|
db: Database,
|
||||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
|
anti_leak: bool,
|
||||||
|
last_activity: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SanctumFs {
|
impl SanctumFs {
|
||||||
pub fn new(db: Database, dek: Zeroizing<[u8; 32]>, format_version: u32) -> Self {
|
pub fn new(db: Database, dek: Zeroizing<[u8; 32]>, format_version: u32) -> Self {
|
||||||
|
Self::with_options(db, dek, format_version, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_options(
|
||||||
|
db: Database,
|
||||||
|
dek: Zeroizing<[u8; 32]>,
|
||||||
|
format_version: u32,
|
||||||
|
anti_leak: bool,
|
||||||
|
) -> Self {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
Self {
|
Self {
|
||||||
db,
|
db,
|
||||||
dek: Arc::new(dek),
|
dek: Arc::new(dek),
|
||||||
format_version,
|
format_version,
|
||||||
|
anti_leak,
|
||||||
|
last_activity: Arc::new(AtomicU64::new(now)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
||||||
|
self.last_activity.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_anti_leak_enabled(&self) -> bool {
|
||||||
|
self.anti_leak
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn touch(&self) {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.last_activity.store(now, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
fn path_to_str(path: &DavPath) -> String {
|
fn path_to_str(path: &DavPath) -> String {
|
||||||
String::from_utf8_lossy(path.as_bytes()).to_string()
|
String::from_utf8_lossy(path.as_bytes()).to_string()
|
||||||
}
|
}
|
||||||
@@ -378,6 +452,25 @@ impl DavFileSystem for SanctumFs {
|
|||||||
) -> FsFuture<'a, Box<dyn DavFile>> {
|
) -> FsFuture<'a, Box<dyn DavFile>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
|
let (parent_path, file_name) = self.split_parent_and_name(&path_str);
|
||||||
|
|
||||||
|
// Anti-Leak Shield: Blockiere Schreib- oder Neuerstellungsversuche für Explorer-Metadaten
|
||||||
|
if self.anti_leak && is_leak_file(file_name) {
|
||||||
|
if options.create
|
||||||
|
|| options.create_new
|
||||||
|
|| options.write
|
||||||
|
|| options.append
|
||||||
|
|| options.truncate
|
||||||
|
{
|
||||||
|
debug!(
|
||||||
|
"Anti-Leak: Blockiere Erstellung/Schreibzugriff für '{}'",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.touch();
|
||||||
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
||||||
|
|
||||||
let existing_node = self
|
let existing_node = self
|
||||||
@@ -417,7 +510,6 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if options.create || options.create_new {
|
if options.create || options.create_new {
|
||||||
let (parent_path, file_name) = self.split_parent_and_name(&path_str);
|
|
||||||
let parent = self
|
let parent = self
|
||||||
.db
|
.db
|
||||||
.resolve_path(parent_path)
|
.resolve_path(parent_path)
|
||||||
@@ -440,7 +532,13 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = SanctumFile::new(node, self.db.clone(), self.dek.clone(), self.format_version);
|
let file = SanctumFile::new(
|
||||||
|
node,
|
||||||
|
self.db.clone(),
|
||||||
|
self.dek.clone(),
|
||||||
|
self.format_version,
|
||||||
|
self.last_activity.clone(),
|
||||||
|
);
|
||||||
Ok(Box::new(file) as Box<dyn DavFile>)
|
Ok(Box::new(file) as Box<dyn DavFile>)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -451,6 +549,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
_meta: ReadDirMeta,
|
_meta: ReadDirMeta,
|
||||||
) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
|
) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.db
|
||||||
@@ -469,6 +568,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = children
|
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = children
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|child| !self.anti_leak || !is_leak_file(&child.name))
|
||||||
.map(|child| {
|
.map(|child| {
|
||||||
Ok(Box::new(SanctumDirEntry {
|
Ok(Box::new(SanctumDirEntry {
|
||||||
name: child.name,
|
name: child.name,
|
||||||
@@ -489,6 +589,9 @@ impl DavFileSystem for SanctumFs {
|
|||||||
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
|
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
|
if path_str != "/" && !path_str.is_empty() {
|
||||||
|
self.touch();
|
||||||
|
}
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.db
|
||||||
.resolve_path(&path_str)
|
.resolve_path(&path_str)
|
||||||
@@ -512,7 +615,14 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
|
let (parent_path, dir_name) = self.split_parent_and_name(&path_str);
|
||||||
|
|
||||||
|
if self.anti_leak && is_leak_file(dir_name) {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.db
|
.db
|
||||||
.resolve_path(&path_str)
|
.resolve_path(&path_str)
|
||||||
@@ -522,7 +632,6 @@ impl DavFileSystem for SanctumFs {
|
|||||||
return Err(FsError::Exists);
|
return Err(FsError::Exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (parent_path, dir_name) = self.split_parent_and_name(&path_str);
|
|
||||||
let parent = self
|
let parent = self
|
||||||
.db
|
.db
|
||||||
.resolve_path(parent_path)
|
.resolve_path(parent_path)
|
||||||
@@ -543,6 +652,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.db
|
||||||
@@ -568,6 +678,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.db
|
||||||
@@ -593,6 +704,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
to: &'a DavPath,
|
to: &'a DavPath,
|
||||||
) -> FsFuture<'a, ()> {
|
) -> FsFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
self.touch();
|
||||||
let from_str = Self::path_to_str(from);
|
let from_str = Self::path_to_str(from);
|
||||||
let to_str = Self::path_to_str(to);
|
let to_str = Self::path_to_str(to);
|
||||||
|
|
||||||
@@ -603,6 +715,11 @@ impl DavFileSystem for SanctumFs {
|
|||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
||||||
|
|
||||||
|
if self.anti_leak && is_leak_file(to_name) {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
let to_parent = self
|
let to_parent = self
|
||||||
.db
|
.db
|
||||||
.resolve_path(to_parent_path)
|
.resolve_path(to_parent_path)
|
||||||
@@ -641,6 +758,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
to: &'a DavPath,
|
to: &'a DavPath,
|
||||||
) -> FsFuture<'a, ()> {
|
) -> FsFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
self.touch();
|
||||||
let from_str = Self::path_to_str(from);
|
let from_str = Self::path_to_str(from);
|
||||||
let to_str = Self::path_to_str(to);
|
let to_str = Self::path_to_str(to);
|
||||||
|
|
||||||
@@ -655,6 +773,11 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
||||||
|
|
||||||
|
if self.anti_leak && is_leak_file(to_name) {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
let to_parent = self
|
let to_parent = self
|
||||||
.db
|
.db
|
||||||
.resolve_path(to_parent_path)
|
.resolve_path(to_parent_path)
|
||||||
@@ -720,3 +843,167 @@ impl DavFileSystem for SanctumFs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION};
|
||||||
|
use dav_server::fs::OpenOptions;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_leak_file() {
|
||||||
|
assert!(is_leak_file("Thumbs.db"));
|
||||||
|
assert!(is_leak_file("thumbs.db"));
|
||||||
|
assert!(is_leak_file("THUMBS.DB"));
|
||||||
|
assert!(is_leak_file("ehthumbs.db"));
|
||||||
|
assert!(is_leak_file("ehthumbs_vista.db"));
|
||||||
|
assert!(is_leak_file("desktop.ini"));
|
||||||
|
assert!(is_leak_file("Desktop.ini"));
|
||||||
|
assert!(is_leak_file("Folder.jpg"));
|
||||||
|
assert!(is_leak_file("albumartsmall.jpg"));
|
||||||
|
assert!(is_leak_file("AlbumArt_{12345}_Large.jpg"));
|
||||||
|
assert!(is_leak_file("AlbumArt_{12345}_Small.jpg"));
|
||||||
|
assert!(is_leak_file("autorun.inf"));
|
||||||
|
assert!(is_leak_file(".ds_store"));
|
||||||
|
|
||||||
|
// Harmlos:
|
||||||
|
assert!(!is_leak_file("secret.txt"));
|
||||||
|
assert!(!is_leak_file("passwords.kdbx"));
|
||||||
|
assert!(!is_leak_file("my_folder.jpg.txt"));
|
||||||
|
assert!(!is_leak_file("desktop_notes.ini.bak"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_fs(anti_leak: bool) -> (SanctumFs, tempfile_placeholder::TempDir) {
|
||||||
|
let temp_dir = tempfile_placeholder::TempDir::new();
|
||||||
|
let db_path = temp_dir.path().join("test_vfs.sanctum");
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: 1024,
|
||||||
|
time_cost: 1,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("testpwd", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let fs = SanctumFs::with_options(db, dek, FORMAT_VERSION, anti_leak);
|
||||||
|
(fs, temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tempfile_placeholder {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
pub struct TempDir(PathBuf);
|
||||||
|
impl TempDir {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let p = std::env::temp_dir().join(format!("sanctum_test_{}", rand::random::<u64>()));
|
||||||
|
std::fs::create_dir_all(&p).unwrap();
|
||||||
|
Self(p)
|
||||||
|
}
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_anti_leak_blocks_creation() {
|
||||||
|
let (fs, _dir) = create_test_fs(true);
|
||||||
|
|
||||||
|
let path = DavPath::new("/desktop.ini").unwrap();
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create_new = true;
|
||||||
|
|
||||||
|
// desktop.ini muss blockiert werden mit Forbidden
|
||||||
|
let res = fs.open(&path, opts).await;
|
||||||
|
assert!(matches!(res, Err(FsError::Forbidden)));
|
||||||
|
|
||||||
|
// create_dir mit Thumbs.db muss auch blockiert werden
|
||||||
|
let dir_path = DavPath::new("/Thumbs.db").unwrap();
|
||||||
|
let res_dir = fs.create_dir(&dir_path).await;
|
||||||
|
assert!(matches!(res_dir, Err(FsError::Forbidden)));
|
||||||
|
|
||||||
|
// Normale Datei muss erlaubt sein
|
||||||
|
let valid_path = DavPath::new("/notes.txt").unwrap();
|
||||||
|
let mut valid_opts = OpenOptions::default();
|
||||||
|
valid_opts.write = true;
|
||||||
|
valid_opts.create_new = true;
|
||||||
|
let res_valid = fs.open(&valid_path, valid_opts).await;
|
||||||
|
assert!(res_valid.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_anti_leak_filters_read_dir() {
|
||||||
|
let (fs_shielded, _dir) = create_test_fs(true);
|
||||||
|
|
||||||
|
// Erstelle eine normale Datei
|
||||||
|
let normal_path = DavPath::new("/legit.txt").unwrap();
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create_new = true;
|
||||||
|
let res = fs_shielded.open(&normal_path, opts).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
|
// Erzwinge direkt in die DB eine Thumbs.db Datei
|
||||||
|
fs_shielded.db.create_node(1, "Thumbs.db", false).unwrap();
|
||||||
|
|
||||||
|
// read_dir mit anti_leak = true darf Thumbs.db NICHT anzeigen
|
||||||
|
let root_path = DavPath::new("/").unwrap();
|
||||||
|
let mut stream = fs_shielded.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
||||||
|
let mut names = Vec::new();
|
||||||
|
while let Some(entry) = stream.next().await {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
names.push(String::from_utf8_lossy(&entry.name()).to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(names.contains(&"legit.txt".to_string()));
|
||||||
|
assert!(!names.contains(&"Thumbs.db".to_string()));
|
||||||
|
|
||||||
|
// Mit unshielded FS (anti_leak = false) muss Thumbs.db sichtbar sein
|
||||||
|
let fs_unshielded = SanctumFs::with_options(
|
||||||
|
fs_shielded.db.clone(),
|
||||||
|
zeroize::Zeroizing::new([0u8; 32]),
|
||||||
|
FORMAT_VERSION,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let mut stream_unshielded = fs_unshielded.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
||||||
|
let mut names_unshielded = Vec::new();
|
||||||
|
while let Some(entry) = stream_unshielded.next().await {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
names_unshielded.push(String::from_utf8_lossy(&entry.name()).to_string());
|
||||||
|
}
|
||||||
|
assert!(names_unshielded.contains(&"Thumbs.db".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_vfs_activity_tracking() {
|
||||||
|
let (fs, _dir) = create_test_fs(true);
|
||||||
|
let act_arc = fs.last_activity();
|
||||||
|
let initial_time = act_arc.load(Ordering::Relaxed);
|
||||||
|
assert!(initial_time > 0);
|
||||||
|
|
||||||
|
// Manuell zurückdatieren
|
||||||
|
act_arc.store(1000, Ordering::Relaxed);
|
||||||
|
assert_eq!(act_arc.load(Ordering::Relaxed), 1000);
|
||||||
|
|
||||||
|
// Nach einem VFS-Zugriff muss die Zeit aktualisiert sein
|
||||||
|
let path = DavPath::new("/test_activity.txt").unwrap();
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create_new = true;
|
||||||
|
let _ = fs.open(&path, opts).await;
|
||||||
|
|
||||||
|
let new_time = act_arc.load(Ordering::Relaxed);
|
||||||
|
assert!(new_time > 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -198,4 +421,13 @@ mod tests {
|
|||||||
assert!(drive.is_ascii_alphabetic());
|
assert!(drive.is_ascii_alphabetic());
|
||||||
assert!(drive >= 'D' && drive <= 'Z');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,4 +498,116 @@ async fn test_sanctum_disaster_recovery_workflow() {
|
|||||||
let _ = std::fs::remove_file(&backup_path);
|
let _ = std::fs::remove_file(&backup_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_anti_leak_and_inactivity_shield() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let container_path: PathBuf =
|
||||||
|
temp_dir.join(format!("test_leak_shield_{}.sanctum", std::process::id()));
|
||||||
|
|
||||||
|
if container_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = "ShieldedContainerPass2026!";
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: 1024,
|
||||||
|
time_cost: 1,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek(password, &salt, &kdf_params).expect("derive kek");
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap dek");
|
||||||
|
|
||||||
|
let db = Database::open(&container_path).expect("open db");
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||||
|
.expect("init schema");
|
||||||
|
db.checkpoint().expect("checkpoint");
|
||||||
|
|
||||||
|
// 1. Erstelle Filesystem mit aktivem Anti-Leak Shield (Standard)
|
||||||
|
let fs = SanctumFs::with_options(db.clone(), dek.clone(), FORMAT_VERSION, true);
|
||||||
|
|
||||||
|
// 2. Windows Explorer Leck-Dateien müssen abgewehrt werden
|
||||||
|
let leak_files = [
|
||||||
|
"/Thumbs.db",
|
||||||
|
"/thumbs.db",
|
||||||
|
"/desktop.ini",
|
||||||
|
"/Desktop.ini",
|
||||||
|
"/Folder.jpg",
|
||||||
|
"/AlbumArt_{A1B2C3D4-E5F6}_Large.jpg",
|
||||||
|
"/albumartsmall.jpg",
|
||||||
|
"/ehthumbs.db",
|
||||||
|
"/autorun.inf",
|
||||||
|
];
|
||||||
|
|
||||||
|
for leak in leak_files {
|
||||||
|
let path = DavPath::new(leak).unwrap();
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create_new = true;
|
||||||
|
let res = fs.open(&path, opts).await;
|
||||||
|
assert!(
|
||||||
|
matches!(res, Err(dav_server::fs::FsError::Forbidden)),
|
||||||
|
"Leak-Datei '{}' muss mit FsError::Forbidden blockiert werden",
|
||||||
|
leak
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordnererstellung mit Leck-Namen muss ebenfalls blockiert werden
|
||||||
|
let leak_dir = DavPath::new("/Thumbs.db").unwrap();
|
||||||
|
let dir_res = fs.create_dir(&leak_dir).await;
|
||||||
|
assert!(matches!(dir_res, Err(dav_server::fs::FsError::Forbidden)));
|
||||||
|
|
||||||
|
// 3. Legitime Dateien müssen reibungslos funktionieren
|
||||||
|
let legit_path = DavPath::new("/top_secret.docx").unwrap();
|
||||||
|
let mut opts_legit = OpenOptions::default();
|
||||||
|
opts_legit.write = true;
|
||||||
|
opts_legit.create_new = true;
|
||||||
|
let mut file = fs.open(&legit_path, opts_legit).await.expect("create legit file");
|
||||||
|
file.write_bytes(Bytes::from_static(b"Sanctum OpSec Shield Test"))
|
||||||
|
.await
|
||||||
|
.expect("write data");
|
||||||
|
file.flush().await.expect("flush data");
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// 4. Inaktivitäts-Tracking überprüfen
|
||||||
|
let activity_tracker = fs.last_activity();
|
||||||
|
let recorded_now = activity_tracker.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
assert!(recorded_now > 0);
|
||||||
|
|
||||||
|
// Zurückdatieren um 300 Sekunden
|
||||||
|
activity_tracker.store(recorded_now - 300, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let past = activity_tracker.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
// Datei lesen
|
||||||
|
let mut opts_read = OpenOptions::default();
|
||||||
|
opts_read.read = true;
|
||||||
|
let mut rf = fs.open(&legit_path, opts_read).await.expect("open legit");
|
||||||
|
let content = rf.read_bytes(25).await.expect("read bytes");
|
||||||
|
assert_eq!(&content[..], b"Sanctum OpSec Shield Test");
|
||||||
|
drop(rf);
|
||||||
|
|
||||||
|
let updated_time = activity_tracker.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
assert!(
|
||||||
|
updated_time > past,
|
||||||
|
"VFS-Aktivität muss den Inaktivitäts-Timer aktualisieren"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Deaktivierter Shield (--no-anti-leak) erlaubt Explorer-Metadaten
|
||||||
|
let unshielded_fs = SanctumFs::with_options(db.clone(), dek.clone(), FORMAT_VERSION, false);
|
||||||
|
let mut unshielded_opts = OpenOptions::default();
|
||||||
|
unshielded_opts.write = true;
|
||||||
|
unshielded_opts.create_new = true;
|
||||||
|
let desktop_ini_path = DavPath::new("/desktop.ini").unwrap();
|
||||||
|
let desktop_res = unshielded_fs.open(&desktop_ini_path, unshielded_opts).await;
|
||||||
|
assert!(
|
||||||
|
desktop_res.is_ok(),
|
||||||
|
"Ohne Shield muss Erstellung von desktop.ini gestattet sein"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aufräumen
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user