feat(opsec-ux): implement HF-01 to HF-04 and VFS carrier protection
- HF-01: eliminate visual leaks between decoy and hidden vaults during mount - HF-02: add secure interactive BIP-39 recovery prompt avoiding shell history - HF-03: implement BIP-39 normalization, word index error pinpointing, and Levenshtein typo suggestions - HF-04: add --stealth mode for silent mounting in high-risk environments - VFS: enforce write, truncate, delete, rename, and directory removal protection for carrier node in decoy vault
This commit is contained in:
+100
@@ -446,6 +446,16 @@ impl SanctumFs {
|
||||
let dek_arc = Arc::new(dek);
|
||||
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
||||
|
||||
// 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.
|
||||
let carrier_node_id = carrier_node_id.or_else(|| {
|
||||
if vault_id == 0 {
|
||||
db.find_carrier_node_id().ok().flatten()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
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(
|
||||
@@ -787,6 +797,13 @@ impl DavFileSystem for SanctumFs {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
// Schutz der Trägerdatei im Decoy Vault: Verzeichnis darf nicht gelöscht werden, wenn es den Carrier enthält!
|
||||
if let Some(carrier_id) = self.carrier_node_id {
|
||||
if self.db.is_descendant_of(carrier_id, node.id).map_err(|_| FsError::GeneralFailure)? {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
self.db
|
||||
.delete_node(node.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
@@ -866,6 +883,10 @@ impl DavFileSystem for SanctumFs {
|
||||
if dest.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
// Schutz der Trägerdatei im Decoy Vault: Überschreiben durch Rename verboten!
|
||||
if self.carrier_node_id == Some(dest.id) {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
self.db
|
||||
.delete_node(dest.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
@@ -901,6 +922,13 @@ impl DavFileSystem for SanctumFs {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
// Schutz der Trägerdatei im Decoy Vault: Überschreiben durch Copy verboten!
|
||||
if let Some(dest) = self.resolve_path(&to_str)? {
|
||||
if self.carrier_node_id == Some(dest.id) {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
let to_parent = self
|
||||
.resolve_path(to_parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
@@ -1124,4 +1152,76 @@ mod tests {
|
||||
let new_time = act_arc.load(Ordering::Relaxed);
|
||||
assert!(new_time > 1000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_carrier_protection_in_decoy_vault() {
|
||||
let (fs, _dir) = create_test_fs(true);
|
||||
// Erstelle eine Carrier-Datei "system_backup.dat"
|
||||
let carrier_node = fs.db.create_node(1, "system_backup.dat", false).unwrap();
|
||||
let carrier_id = carrier_node.id;
|
||||
|
||||
// Erstelle FS mit bekanntem carrier_node_id
|
||||
let protected_fs = SanctumFs::with_carrier(
|
||||
fs.db.clone(),
|
||||
(*fs.dek).clone(),
|
||||
None,
|
||||
Some(carrier_id),
|
||||
FORMAT_VERSION,
|
||||
true,
|
||||
0,
|
||||
);
|
||||
|
||||
let carrier_path = DavPath::new("/system_backup.dat").unwrap();
|
||||
|
||||
// 1. Lesen muss erlaubt sein
|
||||
let mut read_opts = OpenOptions::default();
|
||||
read_opts.read = true;
|
||||
assert!(protected_fs.open(&carrier_path, read_opts).await.is_ok());
|
||||
|
||||
// 2. Schreiben / Truncate muss verboten sein (FsError::Forbidden)
|
||||
let mut write_opts = OpenOptions::default();
|
||||
write_opts.write = true;
|
||||
assert!(matches!(
|
||||
protected_fs.open(&carrier_path, write_opts).await,
|
||||
Err(FsError::Forbidden)
|
||||
));
|
||||
|
||||
let mut trunc_opts = OpenOptions::default();
|
||||
trunc_opts.truncate = true;
|
||||
assert!(matches!(
|
||||
protected_fs.open(&carrier_path, trunc_opts).await,
|
||||
Err(FsError::Forbidden)
|
||||
));
|
||||
|
||||
// 3. Löschen der Carrier-Datei muss verboten sein
|
||||
assert!(matches!(
|
||||
protected_fs.remove_file(&carrier_path).await,
|
||||
Err(FsError::Forbidden)
|
||||
));
|
||||
|
||||
// 4. Umbenennen der Carrier-Datei muss verboten sein
|
||||
let rename_target = DavPath::new("/renamed_backup.dat").unwrap();
|
||||
assert!(matches!(
|
||||
protected_fs.rename(&carrier_path, &rename_target).await,
|
||||
Err(FsError::Forbidden)
|
||||
));
|
||||
|
||||
// 5. Überschreiben der Carrier-Datei durch Rename einer anderen Datei muss verboten sein
|
||||
let other_path = DavPath::new("/other.txt").unwrap();
|
||||
let mut other_opts = OpenOptions::default();
|
||||
other_opts.write = true;
|
||||
other_opts.create_new = true;
|
||||
protected_fs.open(&other_path, other_opts).await.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
protected_fs.rename(&other_path, &carrier_path).await,
|
||||
Err(FsError::Forbidden)
|
||||
));
|
||||
|
||||
// 6. Überschreiben der Carrier-Datei durch Copy einer anderen Datei muss verboten sein
|
||||
assert!(matches!(
|
||||
protected_fs.copy(&other_path, &carrier_path).await,
|
||||
Err(FsError::Forbidden)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user