fix(vfs): V-03 — raii memory lock guard prevents premature memory unlock on fs clone
This commit is contained in:
+19
-6
@@ -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.
|
/// 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)?;
|
validate_kdf_params(params)?;
|
||||||
|
|
||||||
let argon2_params = Params::new(
|
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 argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||||
let mut kek = Zeroizing::new([0u8; 32]);
|
let mut kek = Zeroizing::new([0u8; 32]);
|
||||||
let _ = crate::windows::lock_memory(kek.as_ptr(), 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);
|
argon2
|
||||||
if let Err(e) = res {
|
.hash_password_into(password.as_bytes(), salt, &mut *kek)
|
||||||
let _ = crate::windows::unlock_memory(kek.as_ptr(), 32);
|
.map_err(|e| anyhow::anyhow!("Argon2id KDF-Berechnung fehlgeschlagen: {e}"))?;
|
||||||
bail!("Argon2id KDF-Berechnung fehlgeschlagen: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(kek)
|
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]> {
|
pub fn generate_dek() -> Zeroizing<[u8; 32]> {
|
||||||
let mut dek = Zeroizing::new([0u8; 32]);
|
let mut dek = Zeroizing::new([0u8; 32]);
|
||||||
let _ = crate::windows::lock_memory(dek.as_ptr(), 32);
|
let _ = crate::windows::lock_memory(dek.as_ptr(), 32);
|
||||||
|
let _lock_guard = ScopedMemoryLock(dek.as_ptr(), 32);
|
||||||
OsRng.fill_bytes(&mut *dek);
|
OsRng.fill_bytes(&mut *dek);
|
||||||
dek
|
dek
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-27
@@ -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
|
// Datei-Handle mit Streaming & Chunk-Pufferung
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -103,7 +139,7 @@ pub struct SanctumFile {
|
|||||||
file_size: u64,
|
file_size: u64,
|
||||||
cursor: u64,
|
cursor: u64,
|
||||||
db: Database,
|
db: Database,
|
||||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
dek: Arc<MemoryLockGuard>,
|
||||||
meta: SanctumMetaData,
|
meta: SanctumMetaData,
|
||||||
// (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)>,
|
||||||
@@ -126,7 +162,7 @@ impl SanctumFile {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
node: NodeRecord,
|
node: NodeRecord,
|
||||||
db: Database,
|
db: Database,
|
||||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
dek: Arc<MemoryLockGuard>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
last_activity: Arc<AtomicU64>,
|
last_activity: Arc<AtomicU64>,
|
||||||
append: bool,
|
append: bool,
|
||||||
@@ -445,9 +481,9 @@ impl DavFile for SanctumFile {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SanctumFs {
|
pub struct SanctumFs {
|
||||||
db: Database,
|
db: Database,
|
||||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
dek: Arc<MemoryLockGuard>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
carrier_dek: Option<Arc<Zeroizing<[u8; 32]>>>,
|
carrier_dek: Option<Arc<MemoryLockGuard>>,
|
||||||
carrier_node_id: Option<i64>,
|
carrier_node_id: Option<i64>,
|
||||||
carrier_fs: Option<CarrierFs>,
|
carrier_fs: Option<CarrierFs>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
@@ -493,10 +529,10 @@ impl SanctumFs {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let dek_arc = Arc::new(dek);
|
let dek_guard = Arc::new(MemoryLockGuard::new(dek));
|
||||||
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
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,
|
// 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.
|
// 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 {
|
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(
|
match CarrierFs::load(
|
||||||
db.clone(),
|
db.clone(),
|
||||||
c_nid,
|
c_nid,
|
||||||
c_dek.clone(),
|
Arc::new((*c_dek.key()).clone()),
|
||||||
dek_arc.clone(),
|
Arc::new((*dek_guard.key()).clone()),
|
||||||
format_version,
|
format_version,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
) {
|
) {
|
||||||
@@ -531,16 +567,10 @@ impl SanctumFs {
|
|||||||
None
|
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 {
|
Self {
|
||||||
db,
|
db,
|
||||||
dek: dek_arc,
|
dek: dek_guard,
|
||||||
carrier_dek: carrier_dek_arc,
|
carrier_dek: carrier_dek_guard,
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
carrier_fs,
|
carrier_fs,
|
||||||
format_version,
|
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 {
|
pub fn vault_id(&self) -> u32 {
|
||||||
self.vault_id
|
self.vault_id
|
||||||
}
|
}
|
||||||
@@ -636,15 +670,6 @@ pub fn validate_path_safety(path: &str) -> Result<(), FsError> {
|
|||||||
Ok(())
|
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 {
|
impl DavFileSystem for SanctumFs {
|
||||||
fn open<'a>(
|
fn open<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
|
|||||||
@@ -154,3 +154,68 @@ fn test_mount_security_multi_auth_and_no_token_in_url() {
|
|||||||
"Saubere URI darf kein Token enthalten"
|
"Saubere URI darf kein Token enthalten"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_vfs_memory_lock_retention_on_clone_v03() {
|
||||||
|
use dav_server::davpath::DavPath;
|
||||||
|
use dav_server::fs::{DavFileSystem, ReadDirMeta};
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use sanctum::vfs::SanctumFs;
|
||||||
|
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let container_path: PathBuf =
|
||||||
|
temp_dir.join(format!("test_v03_lock_{}.sanctum", std::process::id()));
|
||||||
|
if container_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("TestPassV03!", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
|
let db = Database::open(&container_path).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 1. Initialisiere SanctumFs
|
||||||
|
let fs = SanctumFs::new(db.clone(), dek, sanctum::crypto::FORMAT_VERSION);
|
||||||
|
assert_eq!(
|
||||||
|
fs.dek_strong_count(),
|
||||||
|
1,
|
||||||
|
"Anfangs muss genau 1 Referenz auf den verriegelten DEK existieren"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Klone SanctumFs (wie bei jeder HTTP/WebDAV-Anfrage im Server)
|
||||||
|
let fs_clone = fs.clone();
|
||||||
|
assert_eq!(
|
||||||
|
fs.dek_strong_count(),
|
||||||
|
2,
|
||||||
|
"Nach dem Klonen müssen 2 Referenzen existieren"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Droppe den Klon
|
||||||
|
drop(fs_clone);
|
||||||
|
|
||||||
|
// 4. V-03: Der Refcount muss nun wieder 1 sein. Der Speicherbereich darf NICHT
|
||||||
|
// vorzeitig über Drop eines Klons entriegelt worden sein!
|
||||||
|
assert_eq!(
|
||||||
|
fs.dek_strong_count(),
|
||||||
|
1,
|
||||||
|
"Nach Drop des Klons muss genau 1 Referenz erhalten bleiben"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verifiziere funktionale Nutzbarkeit nach Drop des Klons
|
||||||
|
let root_path = DavPath::new("/").unwrap();
|
||||||
|
let mut entries = fs.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
||||||
|
assert!(entries.next().await.is_none());
|
||||||
|
|
||||||
|
drop(fs);
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user