fix(vfs): V-03 — raii memory lock guard prevents premature memory unlock on fs clone

This commit is contained in:
2026-09-19 09:10:35 +02:00
parent f9733dfc73
commit 52859623c2
3 changed files with 136 additions and 33 deletions
+19 -6
View File
@@ -104,7 +104,20 @@ pub fn check_password_prefix_collision(pass0: &str, pass1: &str) -> Result<()> {
}
/// Leitet aus dem Master-Passwort und dem Salt einen 256-Bit Key Encryption Key (KEK) via Argon2id ab.
pub fn derive_kek(password: &str, salt: &[u8], params: &KdfParams) -> Result<Zeroizing<[u8; 32]>> {
/// RAII-Guard für kurzzeitige Stack-Puffer, um Lock-Leaks bei Fehlern oder Rückgabe zu verhindern (V-03).
struct ScopedMemoryLock(*const u8, usize);
impl Drop for ScopedMemoryLock {
fn drop(&mut self) {
crate::windows::unlock_memory(self.0, self.1);
}
}
pub fn derive_kek(
password: &str,
salt: &[u8; 16],
params: &KdfParams,
) -> Result<Zeroizing<[u8; 32]>> {
validate_kdf_params(params)?;
let argon2_params = Params::new(
@@ -118,12 +131,11 @@ pub fn derive_kek(password: &str, salt: &[u8], params: &KdfParams) -> Result<Zer
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
let mut kek = Zeroizing::new([0u8; 32]);
let _ = crate::windows::lock_memory(kek.as_ptr(), 32);
let _lock_guard = ScopedMemoryLock(kek.as_ptr(), 32);
let res = argon2.hash_password_into(password.as_bytes(), salt, &mut *kek);
if let Err(e) = res {
let _ = crate::windows::unlock_memory(kek.as_ptr(), 32);
bail!("Argon2id KDF-Berechnung fehlgeschlagen: {e}");
}
argon2
.hash_password_into(password.as_bytes(), salt, &mut *kek)
.map_err(|e| anyhow::anyhow!("Argon2id KDF-Berechnung fehlgeschlagen: {e}"))?;
Ok(kek)
}
@@ -132,6 +144,7 @@ pub fn derive_kek(password: &str, salt: &[u8], params: &KdfParams) -> Result<Zer
pub fn generate_dek() -> Zeroizing<[u8; 32]> {
let mut dek = Zeroizing::new([0u8; 32]);
let _ = crate::windows::lock_memory(dek.as_ptr(), 32);
let _lock_guard = ScopedMemoryLock(dek.as_ptr(), 32);
OsRng.fill_bytes(&mut *dek);
dek
}
+52 -27
View File
@@ -94,6 +94,42 @@ impl DavDirEntry for SanctumDirEntry {
}
}
/// RAII-Guard für im physischen RAM verriegelte Schlüssel (VirtualLock / mlock).
/// Entriegelt den Speicherbereich via VirtualUnlock / munlock erst beim Drop der letzten verbleibenden Referenz (V-03).
#[derive(Debug)]
pub struct MemoryLockGuard(Zeroizing<[u8; 32]>);
impl MemoryLockGuard {
pub fn new(key: Zeroizing<[u8; 32]>) -> Self {
crate::windows::lock_memory(key.as_ptr(), 32);
Self(key)
}
pub fn key(&self) -> &Zeroizing<[u8; 32]> {
&self.0
}
}
impl std::ops::Deref for MemoryLockGuard {
type Target = Zeroizing<[u8; 32]>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<[u8; 32]> for MemoryLockGuard {
fn as_ref(&self) -> &[u8; 32] {
&self.0
}
}
impl Drop for MemoryLockGuard {
fn drop(&mut self) {
crate::windows::unlock_memory(self.0.as_ptr(), 32);
}
}
// ---------------------------------------------------------------------------
// Datei-Handle mit Streaming & Chunk-Pufferung
// ---------------------------------------------------------------------------
@@ -103,7 +139,7 @@ pub struct SanctumFile {
file_size: u64,
cursor: u64,
db: Database,
dek: Arc<Zeroizing<[u8; 32]>>,
dek: Arc<MemoryLockGuard>,
meta: SanctumMetaData,
// (chunk_index, decrypted_payload, is_dirty)
cached_chunk: Option<(u32, Vec<u8>, bool)>,
@@ -126,7 +162,7 @@ impl SanctumFile {
pub fn new(
node: NodeRecord,
db: Database,
dek: Arc<Zeroizing<[u8; 32]>>,
dek: Arc<MemoryLockGuard>,
format_version: u32,
last_activity: Arc<AtomicU64>,
append: bool,
@@ -445,9 +481,9 @@ impl DavFile for SanctumFile {
#[derive(Clone)]
pub struct SanctumFs {
db: Database,
dek: Arc<Zeroizing<[u8; 32]>>,
dek: Arc<MemoryLockGuard>,
#[allow(dead_code)]
carrier_dek: Option<Arc<Zeroizing<[u8; 32]>>>,
carrier_dek: Option<Arc<MemoryLockGuard>>,
carrier_node_id: Option<i64>,
carrier_fs: Option<CarrierFs>,
format_version: u32,
@@ -493,10 +529,10 @@ impl SanctumFs {
.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 dek_guard = Arc::new(MemoryLockGuard::new(dek));
let carrier_dek_guard = carrier_dek.map(|k| Arc::new(MemoryLockGuard::new(k)));
let db = db.with_session(vault_id, (*dek_arc).clone());
let db = db.with_session(vault_id, (*dek_guard.key()).clone());
// Im Decoy-Vault (Slot 0): Stelle sicher, dass carrier_node_id stets bekannt ist,
// um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen.
@@ -509,12 +545,12 @@ impl SanctumFs {
});
let carrier_fs = if vault_id == 1 {
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_arc, carrier_node_id) {
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_guard, carrier_node_id) {
match CarrierFs::load(
db.clone(),
c_nid,
c_dek.clone(),
dek_arc.clone(),
Arc::new((*c_dek.key()).clone()),
Arc::new((*dek_guard.key()).clone()),
format_version,
anti_leak,
) {
@@ -531,16 +567,10 @@ impl SanctumFs {
None
};
// Forensischer RAM-Paging-Schutz via VirtualLock (verhindert Auslagerung in pagefile.sys)
crate::windows::lock_memory(dek_arc.as_ptr(), 32);
if let Some(ref c_dek) = carrier_dek_arc {
crate::windows::lock_memory(c_dek.as_ptr(), 32);
}
Self {
db,
dek: dek_arc,
carrier_dek: carrier_dek_arc,
dek: dek_guard,
carrier_dek: carrier_dek_guard,
carrier_node_id,
carrier_fs,
format_version,
@@ -550,6 +580,10 @@ impl SanctumFs {
}
}
pub fn dek_strong_count(&self) -> usize {
Arc::strong_count(&self.dek)
}
pub fn vault_id(&self) -> u32 {
self.vault_id
}
@@ -636,15 +670,6 @@ pub fn validate_path_safety(path: &str) -> Result<(), FsError> {
Ok(())
}
impl Drop for SanctumFs {
fn drop(&mut self) {
crate::windows::unlock_memory(self.dek.as_ptr(), 32);
if let Some(ref c_dek) = self.carrier_dek {
crate::windows::unlock_memory(c_dek.as_ptr(), 32);
}
}
}
impl DavFileSystem for SanctumFs {
fn open<'a>(
&'a self,