fix(vfs): V-04 — structured anti-leak shield rules and custom filter list
This commit is contained in:
+53
-9
@@ -23,7 +23,7 @@ use zeroize::{Zeroize, Zeroizing};
|
|||||||
|
|
||||||
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
||||||
use crate::storage::Database;
|
use crate::storage::Database;
|
||||||
use crate::vfs::{is_leak_file, SanctumDirEntry, SanctumMetaData};
|
use crate::vfs::{is_leak_file_with_custom, SanctumDirEntry, SanctumMetaData};
|
||||||
|
|
||||||
pub const CARRIER_MAGIC: &[u8; 8] = b"SANCTCAR";
|
pub const CARRIER_MAGIC: &[u8; 8] = b"SANCTCAR";
|
||||||
pub const CARRIER_VERSION: u32 = 1;
|
pub const CARRIER_VERSION: u32 = 1;
|
||||||
@@ -271,11 +271,16 @@ pub struct CarrierFsInner {
|
|||||||
pub dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
pub dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
||||||
pub format_version: u32,
|
pub format_version: u32,
|
||||||
pub anti_leak: bool,
|
pub anti_leak: bool,
|
||||||
|
pub custom_leak_rules: Arc<Vec<String>>,
|
||||||
|
pub leak_counter: Arc<AtomicU64>,
|
||||||
pub manifest: CarrierManifest,
|
pub manifest: CarrierManifest,
|
||||||
pub last_activity: Arc<AtomicU64>,
|
pub last_activity: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CarrierFsInner {
|
impl CarrierFsInner {
|
||||||
|
pub fn is_leak(&self, name: &str) -> bool {
|
||||||
|
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
||||||
|
}
|
||||||
pub fn save_manifest(&mut self) -> Result<()> {
|
pub fn save_manifest(&mut self) -> Result<()> {
|
||||||
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
||||||
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
||||||
@@ -369,14 +374,15 @@ pub struct CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CarrierFs {
|
impl CarrierFs {
|
||||||
/// Lädt ein bestehendes Carrier-Dateisystem aus Block 0 der Trägerdatei.
|
/// Lädt ein bestehendes Carrier-Dateisystem mit benutzerdefinierten Anti-Leak-Regeln (V-04).
|
||||||
pub fn load(
|
pub fn load_with_leak_rules(
|
||||||
db: Database,
|
db: Database,
|
||||||
carrier_node_id: i64,
|
carrier_node_id: i64,
|
||||||
dek_outer: Arc<Zeroizing<[u8; 32]>>,
|
dek_outer: Arc<Zeroizing<[u8; 32]>>,
|
||||||
dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
|
custom_leak_rules: Vec<String>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let manifest_bytes = read_carrier_block(
|
let manifest_bytes = read_carrier_block(
|
||||||
&db,
|
&db,
|
||||||
@@ -408,6 +414,8 @@ impl CarrierFs {
|
|||||||
dek_inner,
|
dek_inner,
|
||||||
format_version,
|
format_version,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
custom_leak_rules: Arc::new(custom_leak_rules),
|
||||||
|
leak_counter: Arc::new(AtomicU64::new(0)),
|
||||||
manifest,
|
manifest,
|
||||||
last_activity: Arc::new(AtomicU64::new(now)),
|
last_activity: Arc::new(AtomicU64::new(now)),
|
||||||
};
|
};
|
||||||
@@ -417,8 +425,33 @@ impl CarrierFs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lädt ein bestehendes Carrier-Dateisystem aus Block 0 der Trägerdatei.
|
||||||
|
pub fn load(
|
||||||
|
db: Database,
|
||||||
|
carrier_node_id: i64,
|
||||||
|
dek_outer: Arc<Zeroizing<[u8; 32]>>,
|
||||||
|
dek_inner: Arc<Zeroizing<[u8; 32]>>,
|
||||||
|
format_version: u32,
|
||||||
|
anti_leak: bool,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Self::load_with_leak_rules(
|
||||||
|
db,
|
||||||
|
carrier_node_id,
|
||||||
|
dek_outer,
|
||||||
|
dek_inner,
|
||||||
|
format_version,
|
||||||
|
anti_leak,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leak_counter(&self) -> Arc<AtomicU64> {
|
||||||
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
inner.leak_counter.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
inner.last_activity.clone()
|
inner.last_activity.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,13 +482,14 @@ impl DavFileSystem for CarrierFs {
|
|||||||
let mut inner = self.inner.lock().unwrap();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
let (parent_path, file_name) = inner.split_parent_and_name(&path_str);
|
let (parent_path, file_name) = inner.split_parent_and_name(&path_str);
|
||||||
|
|
||||||
if inner.anti_leak && is_leak_file(file_name) {
|
if inner.is_leak(file_name) {
|
||||||
if options.create
|
if options.create
|
||||||
|| options.create_new
|
|| options.create_new
|
||||||
|| options.write
|
|| options.write
|
||||||
|| options.append
|
|| options.append
|
||||||
|| options.truncate
|
|| options.truncate
|
||||||
{
|
{
|
||||||
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,7 +588,14 @@ impl DavFileSystem for CarrierFs {
|
|||||||
.inodes
|
.inodes
|
||||||
.values()
|
.values()
|
||||||
.filter(|child| child.parent_id == Some(node.id))
|
.filter(|child| child.parent_id == Some(node.id))
|
||||||
.filter(|child| !inner.anti_leak || !is_leak_file(&child.name))
|
.filter(|child| {
|
||||||
|
if inner.is_leak(&child.name) {
|
||||||
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(|child| {
|
.map(|child| {
|
||||||
Ok(Box::new(SanctumDirEntry {
|
Ok(Box::new(SanctumDirEntry {
|
||||||
name: child.name.clone(),
|
name: child.name.clone(),
|
||||||
@@ -601,7 +642,8 @@ impl DavFileSystem for CarrierFs {
|
|||||||
let mut inner = self.inner.lock().unwrap();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
let (parent_path, dir_name) = inner.split_parent_and_name(&path_str);
|
let (parent_path, dir_name) = inner.split_parent_and_name(&path_str);
|
||||||
|
|
||||||
if inner.anti_leak && is_leak_file(dir_name) {
|
if inner.is_leak(dir_name) {
|
||||||
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,7 +754,8 @@ impl DavFileSystem for CarrierFs {
|
|||||||
let node = inner.resolve_path(&from_str).ok_or(FsError::NotFound)?;
|
let node = inner.resolve_path(&from_str).ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str);
|
let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str);
|
||||||
if inner.anti_leak && is_leak_file(to_name) {
|
if inner.is_leak(to_name) {
|
||||||
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +819,8 @@ impl DavFileSystem for CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str);
|
let (to_parent_path, to_name) = inner.split_parent_and_name(&to_str);
|
||||||
if inner.anti_leak && is_leak_file(to_name) {
|
if inner.is_leak(to_name) {
|
||||||
|
inner.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ enum Commands {
|
|||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_anti_leak: bool,
|
no_anti_leak: bool,
|
||||||
|
|
||||||
|
/// Pfad zu einer Textdatei mit zusätzlichen Anti-Leak Filterregeln (V-04)
|
||||||
|
#[arg(long, value_name = "FILE")]
|
||||||
|
anti_leak_list: Option<PathBuf>,
|
||||||
|
|
||||||
/// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben)
|
/// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben)
|
||||||
#[arg(short, long, default_value_t = false)]
|
#[arg(short, long, default_value_t = false)]
|
||||||
stealth: bool,
|
stealth: bool,
|
||||||
@@ -1618,6 +1622,7 @@ async fn run() -> Result<()> {
|
|||||||
idle_timeout,
|
idle_timeout,
|
||||||
no_screen_lock,
|
no_screen_lock,
|
||||||
no_anti_leak,
|
no_anti_leak,
|
||||||
|
anti_leak_list,
|
||||||
stealth,
|
stealth,
|
||||||
} => {
|
} => {
|
||||||
let drive_char = match drive {
|
let drive_char = match drive {
|
||||||
@@ -1670,6 +1675,7 @@ async fn run() -> Result<()> {
|
|||||||
idle_timeout,
|
idle_timeout,
|
||||||
lock_on_screen_lock,
|
lock_on_screen_lock,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
anti_leak_list.as_deref(),
|
||||||
stealth,
|
stealth,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
+32
-1
@@ -84,6 +84,7 @@ pub async fn mount_container(
|
|||||||
idle_timeout: Option<u64>,
|
idle_timeout: Option<u64>,
|
||||||
lock_on_screen_lock: bool,
|
lock_on_screen_lock: bool,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
|
anti_leak_list: Option<&Path>,
|
||||||
stealth: bool,
|
stealth: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
@@ -191,16 +192,38 @@ pub async fn mount_container(
|
|||||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
|
||||||
let session_token = hex::encode(token_bytes);
|
let session_token = hex::encode(token_bytes);
|
||||||
|
|
||||||
|
// Benutzerdefinierte Anti-Leak Filterregeln laden (V-04)
|
||||||
|
let custom_leak_rules = if let Some(path) = anti_leak_list {
|
||||||
|
match crate::vfs::load_anti_leak_list(path) {
|
||||||
|
Ok(rules) => {
|
||||||
|
if !stealth {
|
||||||
|
println!(
|
||||||
|
" • Anti-Leak Liste: {} benutzerdefinierte Regeln geladen",
|
||||||
|
rules.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rules
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
bail!("Fehler beim Laden der Anti-Leak-Regeln: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Carrier-Routing)
|
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Carrier-Routing)
|
||||||
let fs = SanctumFs::with_carrier(
|
let fs = SanctumFs::with_carrier_and_leak_rules(
|
||||||
db.clone(),
|
db.clone(),
|
||||||
dek,
|
dek,
|
||||||
carrier_dek,
|
carrier_dek,
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
version,
|
version,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
custom_leak_rules,
|
||||||
vault_id,
|
vault_id,
|
||||||
);
|
);
|
||||||
|
let fs_leak_counter = fs.leak_counter();
|
||||||
let last_activity = fs.last_activity();
|
let last_activity = fs.last_activity();
|
||||||
let dav_server = DavHandler::builder()
|
let dav_server = DavHandler::builder()
|
||||||
.filesystem(Box::new(fs))
|
.filesystem(Box::new(fs))
|
||||||
@@ -584,6 +607,14 @@ pub async fn mount_container(
|
|||||||
println!("{}", ui::green("OK"));
|
println!("{}", ui::green("OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let blocked_leaks = fs_leak_counter.load(Ordering::Relaxed);
|
||||||
|
if blocked_leaks > 0 {
|
||||||
|
println!(
|
||||||
|
" • Anti-Leak Shield: {} Zugriffe auf Metadaten-/Leak-Dateien blockiert.",
|
||||||
|
ui::cyan(&blocked_leaks.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!(
|
println!(
|
||||||
"{} Sanctum Container wurde sicher und vollständig geschlossen.",
|
"{} Sanctum Container wurde sicher und vollständig geschlossen.",
|
||||||
|
|||||||
+264
-20
@@ -4,6 +4,8 @@ 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};
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
use bytes::{Buf, Bytes, BytesMut};
|
use bytes::{Buf, Bytes, BytesMut};
|
||||||
use dav_server::{
|
use dav_server::{
|
||||||
davpath::DavPath,
|
davpath::DavPath,
|
||||||
@@ -20,23 +22,128 @@ use crate::carrier::CarrierFs;
|
|||||||
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-
|
/// Exakte Namen von Explorer-, OS- und Desktop-Metadaten (V-04).
|
||||||
/// oder Thumbnail-Dateien gehört (z. B. Thumbs.db, desktop.ini), die standardmäßig
|
pub const LEAK_EXACT_NAMES: &[&str] = &[
|
||||||
/// im Container blockiert und verborgen werden (Anti-Leak Shield).
|
"thumbs.db",
|
||||||
|
"ehthumbs.db",
|
||||||
|
"ehthumbs_vista.db",
|
||||||
|
"desktop.ini",
|
||||||
|
"folder.jpg",
|
||||||
|
"albumartsmall.jpg",
|
||||||
|
"autorun.inf",
|
||||||
|
".ds_store",
|
||||||
|
".directory",
|
||||||
|
".fseventsd",
|
||||||
|
".spotlight-v100",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Präfixe bekannter temporärer Metadaten und Lock-Dateien (V-04).
|
||||||
|
pub const LEAK_PREFIXES: &[&str] = &[
|
||||||
|
"~$", // MS Office temporäre Lock-Dateien (z. B. ~$Document.docx)
|
||||||
|
"._", // macOS AppleDouble Metadaten-Dateien (z. B. ._Document.pdf)
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Suffixe und Dateiendungen temporärer Caches und unvollständiger Downloads (V-04).
|
||||||
|
pub const LEAK_SUFFIXES: &[&str] = &[
|
||||||
|
".tmp",
|
||||||
|
".temp",
|
||||||
|
".crdownload", // Google Chrome temporärer Download
|
||||||
|
".part", // Mozilla Firefox unvollständiger Download
|
||||||
|
".partial",
|
||||||
|
"~", // Linux/UNIX Editor-Backups (Vim, Emacs, Gedit)
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Prüft, ob ein Dateiname zu den typischen Windows Explorer-, OS- oder Anwendungs-
|
||||||
|
/// Metadaten-, Cache- oder Lock-Dateien gehört, die standardmäßig im Container
|
||||||
|
/// blockiert und verborgen werden (Anti-Leak Shield, V-04).
|
||||||
pub fn is_leak_file(filename: &str) -> bool {
|
pub fn is_leak_file(filename: &str) -> bool {
|
||||||
let lower = filename.trim().to_ascii_lowercase();
|
is_leak_file_with_custom(filename, &[])
|
||||||
match lower.as_str() {
|
}
|
||||||
"thumbs.db" | "ehthumbs.db" | "ehthumbs_vista.db" | "desktop.ini" | "folder.jpg"
|
|
||||||
| "albumartsmall.jpg" | "autorun.inf" | ".ds_store" => true,
|
/// Prüft, ob ein Dateiname gemäß der Standardregeln oder benutzerdefinierten Regeln (V-04)
|
||||||
_ => {
|
/// als Leak-Datei blockiert werden soll.
|
||||||
if lower.starts_with("albumart") && (lower.ends_with(".jpg") || lower.ends_with(".ini"))
|
pub fn is_leak_file_with_custom(filename: &str, custom_rules: &[String]) -> bool {
|
||||||
{
|
let trimmed = filename.trim();
|
||||||
true
|
if trimmed.is_empty() {
|
||||||
} else {
|
return false;
|
||||||
false
|
}
|
||||||
}
|
|
||||||
|
// 1. NTFS Alternate Data Streams (ADS) wie "file.txt:Zone.Identifier"
|
||||||
|
if trimmed.contains(':') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower = trimmed.to_ascii_lowercase();
|
||||||
|
|
||||||
|
// 2. Exakte Namen
|
||||||
|
for &exact in LEAK_EXACT_NAMES {
|
||||||
|
if lower == exact {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Präfixe
|
||||||
|
for &prefix in LEAK_PREFIXES {
|
||||||
|
if lower.starts_with(prefix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Suffixe
|
||||||
|
for &suffix in LEAK_SUFFIXES {
|
||||||
|
if lower.ends_with(suffix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Spezielle Muster
|
||||||
|
if lower.starts_with("albumart") && (lower.ends_with(".jpg") || lower.ends_with(".ini")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if lower.starts_with(".trash") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Benutzerdefinierte Regeln
|
||||||
|
for custom in custom_rules {
|
||||||
|
let pat = custom.trim().to_ascii_lowercase();
|
||||||
|
if pat.is_empty() || pat.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if pat.starts_with('*') && pat.ends_with('*') && pat.len() > 2 {
|
||||||
|
let sub = &pat[1..pat.len() - 1];
|
||||||
|
if lower.contains(sub) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if pat.starts_with('*') {
|
||||||
|
if lower.ends_with(&pat[1..]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if pat.ends_with('*') {
|
||||||
|
if lower.starts_with(&pat[..pat.len() - 1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if lower == pat {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt eine benutzerdefinierte Liste von Anti-Leak Filterregeln aus einer Textdatei (V-04).
|
||||||
|
pub fn load_anti_leak_list<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||||
|
let p = path.as_ref();
|
||||||
|
let content = std::fs::read_to_string(p)
|
||||||
|
.with_context(|| format!("Konnte Anti-Leak-Listendatei '{}' nicht lesen", p.display()))?;
|
||||||
|
let mut rules = Vec::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.is_empty() && !trimmed.starts_with('#') {
|
||||||
|
rules.push(trimmed.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(rules)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -516,6 +623,8 @@ pub struct SanctumFs {
|
|||||||
carrier_fs: Option<CarrierFs>,
|
carrier_fs: Option<CarrierFs>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
|
custom_leak_rules: Arc<Vec<String>>,
|
||||||
|
leak_counter: Arc<AtomicU64>,
|
||||||
vault_id: u32,
|
vault_id: u32,
|
||||||
last_activity: Arc<AtomicU64>,
|
last_activity: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
@@ -534,6 +643,25 @@ impl SanctumFs {
|
|||||||
Self::with_vault(db, dek, format_version, anti_leak, 0)
|
Self::with_vault(db, dek, format_version, anti_leak, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_options_and_leak_rules(
|
||||||
|
db: Database,
|
||||||
|
dek: Zeroizing<[u8; 32]>,
|
||||||
|
format_version: u32,
|
||||||
|
anti_leak: bool,
|
||||||
|
custom_leak_rules: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_carrier_and_leak_rules(
|
||||||
|
db,
|
||||||
|
dek,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
format_version,
|
||||||
|
anti_leak,
|
||||||
|
custom_leak_rules,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_vault(
|
pub fn with_vault(
|
||||||
db: Database,
|
db: Database,
|
||||||
dek: Zeroizing<[u8; 32]>,
|
dek: Zeroizing<[u8; 32]>,
|
||||||
@@ -552,6 +680,28 @@ impl SanctumFs {
|
|||||||
format_version: u32,
|
format_version: u32,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
vault_id: u32,
|
vault_id: u32,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_carrier_and_leak_rules(
|
||||||
|
db,
|
||||||
|
dek,
|
||||||
|
carrier_dek,
|
||||||
|
carrier_node_id,
|
||||||
|
format_version,
|
||||||
|
anti_leak,
|
||||||
|
Vec::new(),
|
||||||
|
vault_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_carrier_and_leak_rules(
|
||||||
|
db: Database,
|
||||||
|
dek: Zeroizing<[u8; 32]>,
|
||||||
|
carrier_dek: Option<Zeroizing<[u8; 32]>>,
|
||||||
|
carrier_node_id: Option<i64>,
|
||||||
|
format_version: u32,
|
||||||
|
anti_leak: bool,
|
||||||
|
custom_leak_rules: Vec<String>,
|
||||||
|
vault_id: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -574,13 +724,14 @@ impl SanctumFs {
|
|||||||
|
|
||||||
let carrier_fs = if vault_id == 1 {
|
let carrier_fs = if vault_id == 1 {
|
||||||
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_guard, carrier_node_id) {
|
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_guard, carrier_node_id) {
|
||||||
match CarrierFs::load(
|
match CarrierFs::load_with_leak_rules(
|
||||||
db.clone(),
|
db.clone(),
|
||||||
c_nid,
|
c_nid,
|
||||||
Arc::new((*c_dek.key()).clone()),
|
Arc::new((*c_dek.key()).clone()),
|
||||||
Arc::new((*dek_guard.key()).clone()),
|
Arc::new((*dek_guard.key()).clone()),
|
||||||
format_version,
|
format_version,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
custom_leak_rules.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(cfs) => Some(cfs),
|
Ok(cfs) => Some(cfs),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -603,6 +754,8 @@ impl SanctumFs {
|
|||||||
carrier_fs,
|
carrier_fs,
|
||||||
format_version,
|
format_version,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
custom_leak_rules: Arc::new(custom_leak_rules),
|
||||||
|
leak_counter: Arc::new(AtomicU64::new(0)),
|
||||||
vault_id,
|
vault_id,
|
||||||
last_activity: Arc::new(AtomicU64::new(now)),
|
last_activity: Arc::new(AtomicU64::new(now)),
|
||||||
}
|
}
|
||||||
@@ -628,6 +781,22 @@ impl SanctumFs {
|
|||||||
self.anti_leak
|
self.anti_leak
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn leak_counter(&self) -> Arc<AtomicU64> {
|
||||||
|
if let Some(ref cfs) = self.carrier_fs {
|
||||||
|
cfs.leak_counter()
|
||||||
|
} else {
|
||||||
|
self.leak_counter.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leak_count(&self) -> u64 {
|
||||||
|
self.leak_counter().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_leak(&self, name: &str) -> bool {
|
||||||
|
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn touch(&self) {
|
pub fn touch(&self) {
|
||||||
if let Some(ref cfs) = self.carrier_fs {
|
if let Some(ref cfs) = self.carrier_fs {
|
||||||
cfs.touch();
|
cfs.touch();
|
||||||
@@ -713,13 +882,14 @@ impl DavFileSystem for SanctumFs {
|
|||||||
let (parent_path, file_name) = self.split_parent_and_name(&path_str);
|
let (parent_path, file_name) = self.split_parent_and_name(&path_str);
|
||||||
|
|
||||||
// Anti-Leak Shield: Blockiere Schreib- oder Neuerstellungsversuche für Explorer-Metadaten
|
// Anti-Leak Shield: Blockiere Schreib- oder Neuerstellungsversuche für Explorer-Metadaten
|
||||||
if self.anti_leak && is_leak_file(file_name) {
|
if self.is_leak(file_name) {
|
||||||
if options.create
|
if options.create
|
||||||
|| options.create_new
|
|| options.create_new
|
||||||
|| options.write
|
|| options.write
|
||||||
|| options.append
|
|| options.append
|
||||||
|| options.truncate
|
|| options.truncate
|
||||||
{
|
{
|
||||||
|
self.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
debug!(
|
debug!(
|
||||||
"Anti-Leak: Blockiere Erstellung/Schreibzugriff für '{}'",
|
"Anti-Leak: Blockiere Erstellung/Schreibzugriff für '{}'",
|
||||||
file_name
|
file_name
|
||||||
@@ -821,7 +991,14 @@ 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))
|
.filter(|child| {
|
||||||
|
if self.is_leak(&child.name) {
|
||||||
|
self.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(|child| {
|
.map(|child| {
|
||||||
Ok(Box::new(SanctumDirEntry {
|
Ok(Box::new(SanctumDirEntry {
|
||||||
name: child.name,
|
name: child.name,
|
||||||
@@ -876,7 +1053,8 @@ impl DavFileSystem for SanctumFs {
|
|||||||
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);
|
let (parent_path, dir_name) = self.split_parent_and_name(&path_str);
|
||||||
|
|
||||||
if self.anti_leak && is_leak_file(dir_name) {
|
if self.is_leak(dir_name) {
|
||||||
|
self.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,7 +1158,8 @@ 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) {
|
if self.is_leak(to_name) {
|
||||||
|
self.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,7 +1218,8 @@ 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) {
|
if self.is_leak(to_name) {
|
||||||
|
self.leak_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,6 +1344,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_leak_file() {
|
fn test_is_leak_file() {
|
||||||
|
// Exakte Namen
|
||||||
assert!(is_leak_file("Thumbs.db"));
|
assert!(is_leak_file("Thumbs.db"));
|
||||||
assert!(is_leak_file("thumbs.db"));
|
assert!(is_leak_file("thumbs.db"));
|
||||||
assert!(is_leak_file("THUMBS.DB"));
|
assert!(is_leak_file("THUMBS.DB"));
|
||||||
@@ -1177,12 +1358,75 @@ mod tests {
|
|||||||
assert!(is_leak_file("AlbumArt_{12345}_Small.jpg"));
|
assert!(is_leak_file("AlbumArt_{12345}_Small.jpg"));
|
||||||
assert!(is_leak_file("autorun.inf"));
|
assert!(is_leak_file("autorun.inf"));
|
||||||
assert!(is_leak_file(".ds_store"));
|
assert!(is_leak_file(".ds_store"));
|
||||||
|
assert!(is_leak_file(".DS_Store"));
|
||||||
|
assert!(is_leak_file(".directory"));
|
||||||
|
assert!(is_leak_file(".fseventsd"));
|
||||||
|
assert!(is_leak_file(".spotlight-v100"));
|
||||||
|
|
||||||
|
// Präfixe (V-04)
|
||||||
|
assert!(is_leak_file("~$MyDocument.docx"));
|
||||||
|
assert!(is_leak_file("._Document.pdf"));
|
||||||
|
|
||||||
|
// Suffixe (V-04)
|
||||||
|
assert!(is_leak_file("temp_file.tmp"));
|
||||||
|
assert!(is_leak_file("cache.temp"));
|
||||||
|
assert!(is_leak_file("video.crdownload"));
|
||||||
|
assert!(is_leak_file("archive.tar.gz.part"));
|
||||||
|
assert!(is_leak_file("bigfile.partial"));
|
||||||
|
assert!(is_leak_file("notes.txt~"));
|
||||||
|
|
||||||
|
// NTFS Alternate Data Streams (ADS, V-04)
|
||||||
|
assert!(is_leak_file("document.pdf:Zone.Identifier"));
|
||||||
|
assert!(is_leak_file("file.exe:$DATA"));
|
||||||
|
|
||||||
|
// Wildcards (.trash*, V-04)
|
||||||
|
assert!(is_leak_file(".trash"));
|
||||||
|
assert!(is_leak_file(".Trash-1000"));
|
||||||
|
assert!(is_leak_file(".trashes"));
|
||||||
|
|
||||||
// Harmlos:
|
// Harmlos:
|
||||||
assert!(!is_leak_file("secret.txt"));
|
assert!(!is_leak_file("secret.txt"));
|
||||||
assert!(!is_leak_file("passwords.kdbx"));
|
assert!(!is_leak_file("passwords.kdbx"));
|
||||||
assert!(!is_leak_file("my_folder.jpg.txt"));
|
assert!(!is_leak_file("my_folder.jpg.txt"));
|
||||||
assert!(!is_leak_file("desktop_notes.ini.bak"));
|
assert!(!is_leak_file("desktop_notes.ini.bak"));
|
||||||
|
assert!(!is_leak_file("temp_report.docx"));
|
||||||
|
assert!(!is_leak_file("part1_chapter.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v04_custom_anti_leak_rules_and_loader() {
|
||||||
|
let custom_rules = vec![
|
||||||
|
"*.secret_log".to_string(),
|
||||||
|
"debug_*".to_string(),
|
||||||
|
"*_temp_*".to_string(),
|
||||||
|
"custom_exact.bin".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(is_leak_file_with_custom("audit.secret_log", &custom_rules));
|
||||||
|
assert!(is_leak_file_with_custom("debug_dump.txt", &custom_rules));
|
||||||
|
assert!(is_leak_file_with_custom("app_temp_cache.dat", &custom_rules));
|
||||||
|
assert!(is_leak_file_with_custom("custom_exact.bin", &custom_rules));
|
||||||
|
|
||||||
|
// Normale Datei wird nicht blockiert
|
||||||
|
assert!(!is_leak_file_with_custom("regular_file.txt", &custom_rules));
|
||||||
|
|
||||||
|
// Test load_anti_leak_list
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let rule_file = temp_dir.path().join("anti_leak_rules.txt");
|
||||||
|
std::fs::write(
|
||||||
|
&rule_file,
|
||||||
|
"# Kommentarzeile\n*.bak\n\n # Noch ein Kommentar\nprivate_*\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let loaded = load_anti_leak_list(&rule_file).unwrap();
|
||||||
|
assert_eq!(loaded.len(), 2);
|
||||||
|
assert_eq!(loaded[0], "*.bak");
|
||||||
|
assert_eq!(loaded[1], "private_*");
|
||||||
|
|
||||||
|
assert!(is_leak_file_with_custom("data.bak", &loaded));
|
||||||
|
assert!(is_leak_file_with_custom("private_keys.pem", &loaded));
|
||||||
|
assert!(!is_leak_file_with_custom("public_data.txt", &loaded));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_test_fs(anti_leak: bool) -> (SanctumFs, tempfile_placeholder::TempDir) {
|
fn create_test_fs(anti_leak: bool) -> (SanctumFs, tempfile_placeholder::TempDir) {
|
||||||
|
|||||||
Reference in New Issue
Block a user