feat(security): implement Phase 2 Modell A (Steganografischer Alibi-Carrier für Plausible Deniability)

This commit is contained in:
2026-09-09 20:22:00 +02:00
parent b8e4dcb614
commit 030ce6a1e5
8 changed files with 1971 additions and 73 deletions
+108 -8
View File
@@ -16,6 +16,7 @@ use futures_util::stream;
use tracing::{debug, error, warn};
use zeroize::Zeroizing;
use crate::carrier::CarrierFs;
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
use crate::storage::{Database, NodeRecord};
@@ -386,6 +387,10 @@ impl DavFile for SanctumFile {
pub struct SanctumFs {
db: Database,
dek: Arc<Zeroizing<[u8; 32]>>,
#[allow(dead_code)]
carrier_dek: Option<Arc<Zeroizing<[u8; 32]>>>,
carrier_node_id: Option<i64>,
carrier_fs: Option<CarrierFs>,
format_version: u32,
anti_leak: bool,
vault_id: u32,
@@ -412,14 +417,55 @@ impl SanctumFs {
format_version: u32,
anti_leak: bool,
vault_id: u32,
) -> Self {
Self::with_carrier(db, dek, None, None, format_version, anti_leak, vault_id)
}
pub fn with_carrier(
db: Database,
dek: Zeroizing<[u8; 32]>,
carrier_dek: Option<Zeroizing<[u8; 32]>>,
carrier_node_id: Option<i64>,
format_version: u32,
anti_leak: bool,
vault_id: u32,
) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let dek_arc = Arc::new(dek);
let carrier_dek_arc = carrier_dek.map(Arc::new);
let carrier_fs = if vault_id == 1 {
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_arc, carrier_node_id) {
match CarrierFs::load(
db.clone(),
c_nid,
c_dek.clone(),
dek_arc.clone(),
format_version,
anti_leak,
) {
Ok(cfs) => Some(cfs),
Err(e) => {
warn!("CarrierFs konnte nicht initialisiert werden: {e}");
None
}
}
} else {
None
}
} else {
None
};
Self {
db,
dek: Arc::new(dek),
dek: dek_arc,
carrier_dek: carrier_dek_arc,
carrier_node_id,
carrier_fs,
format_version,
anti_leak,
vault_id,
@@ -432,7 +478,11 @@ impl SanctumFs {
}
pub fn last_activity(&self) -> Arc<AtomicU64> {
self.last_activity.clone()
if let Some(ref cfs) = self.carrier_fs {
cfs.last_activity()
} else {
self.last_activity.clone()
}
}
pub fn is_anti_leak_enabled(&self) -> bool {
@@ -440,11 +490,15 @@ impl SanctumFs {
}
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);
if let Some(ref cfs) = self.carrier_fs {
cfs.touch();
} else {
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 {
@@ -493,6 +547,10 @@ impl DavFileSystem for SanctumFs {
path: &'a DavPath,
options: OpenOptions,
) -> FsFuture<'a, Box<dyn DavFile>> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.open(path, options);
}
Box::pin(async move {
let path_str = Self::path_to_str(path);
let (parent_path, file_name) = self.split_parent_and_name(&path_str);
@@ -520,6 +578,11 @@ impl DavFileSystem for SanctumFs {
let node = match existing_node {
Some(n) => {
// Schutz der Trägerdatei im Decoy Vault: Keine Schreib- oder Truncate-Operationen erlaubt!
if self.carrier_node_id == Some(n.id) && (options.write || options.truncate || options.append) {
return Err(FsError::Forbidden);
}
if n.is_dir && (options.write || options.append) {
return Err(FsError::Forbidden);
}
@@ -579,8 +642,12 @@ impl DavFileSystem for SanctumFs {
fn read_dir<'a>(
&'a self,
path: &'a DavPath,
_meta: ReadDirMeta,
meta: ReadDirMeta,
) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.read_dir(path, meta);
}
Box::pin(async move {
self.touch();
let path_str = Self::path_to_str(path);
@@ -615,6 +682,10 @@ impl DavFileSystem for SanctumFs {
}
fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.metadata(path);
}
Box::pin(async move {
let path_str = Self::path_to_str(path);
if path_str != "/" && !path_str.is_empty() {
@@ -636,10 +707,17 @@ impl DavFileSystem for SanctumFs {
}
fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.symlink_metadata(path);
}
self.metadata(path)
}
fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.create_dir(path);
}
Box::pin(async move {
self.touch();
let path_str = Self::path_to_str(path);
@@ -668,6 +746,10 @@ impl DavFileSystem for SanctumFs {
}
fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.remove_dir(path);
}
Box::pin(async move {
self.touch();
let path_str = Self::path_to_str(path);
@@ -693,6 +775,10 @@ impl DavFileSystem for SanctumFs {
}
fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.remove_file(path);
}
Box::pin(async move {
self.touch();
let path_str = Self::path_to_str(path);
@@ -704,6 +790,11 @@ impl DavFileSystem for SanctumFs {
return Err(FsError::Forbidden);
}
// Schutz der Trägerdatei im Decoy Vault: Löschen verboten!
if self.carrier_node_id == Some(node.id) {
return Err(FsError::Forbidden);
}
self.db
.delete_node(node.id)
.map_err(|_| FsError::GeneralFailure)?;
@@ -717,6 +808,10 @@ impl DavFileSystem for SanctumFs {
from: &'a DavPath,
to: &'a DavPath,
) -> FsFuture<'a, ()> {
if let Some(ref cfs) = self.carrier_fs {
return cfs.rename(from, to);
}
Box::pin(async move {
self.touch();
let from_str = Self::path_to_str(from);
@@ -726,6 +821,11 @@ impl DavFileSystem for SanctumFs {
.resolve_path(&from_str)?
.ok_or(FsError::NotFound)?;
// Schutz der Trägerdatei im Decoy Vault: Umbenennen verboten!
if self.carrier_node_id == Some(node.id) {
return Err(FsError::Forbidden);
}
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
if self.anti_leak && is_leak_file(to_name) {