fix(carrier): C-04 — document hidden vault capacity limit and add 80% mount warning

This commit is contained in:
2026-09-19 23:17:04 +02:00
parent c606fa8132
commit c21d63ad28
6 changed files with 201 additions and 6 deletions
+1
View File
@@ -37,6 +37,7 @@ Beim Erstellen eines Tresors wählst du zwischen zwei Sicherheitsstufen:
1. **Decoy-Passwort**: Öffnet den äußeren Safe (enthält eine scheinbare Backup-Datei `system_backup.dat`). 1. **Decoy-Passwort**: Öffnet den äußeren Safe (enthält eine scheinbare Backup-Datei `system_backup.dat`).
2. **Hidden-Passwort**: Öffnet den geheimen Zweittresor (Second Safe). 2. **Hidden-Passwort**: Öffnet den geheimen Zweittresor (Second Safe).
* *(Hinweis: Schützt vor Schulterblick im Alltag; keine forensische Abstreitbarkeit gegen behördliche Entropieanalyse).* * *(Hinweis: Schützt vor Schulterblick im Alltag; keine forensische Abstreitbarkeit gegen behördliche Entropieanalyse).*
* **Kapazitätsgrenze (C-04)**: Empfohlene Obergrenze ca. 7.000 Dateien/Verzeichnisse im Hidden Vault (~1 MB Manifest-Block). Sanctum warnt beim Mounten automatisch bei über 80% Füllstand.
* **Wichtig**: Notiere dir die ausgegebenen **24 Wörter des Notfallschlüssels (BIP-39)** auf der untenstehenden Notfallkarte! * **Wichtig**: Notiere dir die ausgegebenen **24 Wörter des Notfallschlüssels (BIP-39)** auf der untenstehenden Notfallkarte!
--- ---
+1
View File
@@ -37,6 +37,7 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen von Chunks zwischen Dateien oder innerhalb einer Datei führt zum Authentifizierungsfehler. Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen von Chunks zwischen Dateien oder innerhalb einer Datei führt zum Authentifizierungsfehler.
- **Dual-Vault (Multi-Slot & Carrier)**: - **Dual-Vault (Multi-Slot & Carrier)**:
Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Second Safe (Hidden Vault) oder CSPRNG-Dummy. Dient dem Schutz vor neugierigen Blicken oder beiläufigem Zwang im Alltag. (Hinweis: Die Trägerdatei besitzt hohe Entropie und ist forensisch nachweisbar; kein Anspruch auf juristisch unnachweisbare Abstreitbarkeit gegen behördliche Beschlagnahme). Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Second Safe (Hidden Vault) oder CSPRNG-Dummy. Dient dem Schutz vor neugierigen Blicken oder beiläufigem Zwang im Alltag. (Hinweis: Die Trägerdatei besitzt hohe Entropie und ist forensisch nachweisbar; kein Anspruch auf juristisch unnachweisbare Abstreitbarkeit gegen behördliche Beschlagnahme).
*Kapazitätsgrenze des Hidden Vault (C-04)*: Das Dateisystem des Hidden Vaults verwaltet seine Inodes in einem dual-block rollierenden Manifest (~1 MB pro Block). Daraus ergibt sich eine empfohlene Obergrenze von ca. 7.000 Dateien und Verzeichnissen im Hidden Vault. Bei Überschreiten von 80% Füllstand (~838 KB) warnt Sanctum beim Einbinden automatisch vor Erreichen der Blockgrenze.
- **Dateinamen-Verschlüsselung**: - **Dateinamen-Verschlüsselung**:
Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert (Legacy-Kompatibilität über `--legacy-names`). Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert (Legacy-Kompatibilität über `--legacy-names`).
- **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**: - **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**:
+37 -6
View File
@@ -27,6 +27,8 @@ 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;
pub const CARRIER_MANIFEST_CAPACITY: usize = CHUNK_SIZE - 64; // 1,048,512 Bytes
pub const CARRIER_MANIFEST_WARN_THRESHOLD: usize = CARRIER_MANIFEST_CAPACITY * 8 / 10; // 838,809 Bytes (80%, C-04)
/// Manifest für das steganografische Dateisystem innerhalb des Alibi-Carriers (Block 0 und 1, C-02). /// Manifest für das steganografische Dateisystem innerhalb des Alibi-Carriers (Block 0 und 1, C-02).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -312,6 +314,15 @@ impl CarrierFsInner {
Ok(()) Ok(())
} }
/// Ermittelt die aktuelle Bytegröße des Manifests, die Kapazität und den Füllstand in Prozent (C-04).
pub fn manifest_usage(&self) -> (usize, usize, f64) {
let len = serde_json::to_vec(&self.manifest)
.map(|v| v.len())
.unwrap_or(0);
let pct = (len as f64 / CARRIER_MANIFEST_CAPACITY as f64) * 100.0;
(len, CARRIER_MANIFEST_CAPACITY, pct)
}
pub fn save_manifest(&mut self) -> Result<()> { pub fn save_manifest(&mut self) -> Result<()> {
self.manifest_save_count += 1; self.manifest_save_count += 1;
self.manifest_dirty = false; self.manifest_dirty = false;
@@ -322,18 +333,20 @@ impl CarrierFsInner {
.unwrap_or(0); .unwrap_or(0);
self.manifest.manifest_generation += 1; self.manifest.manifest_generation += 1;
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() > CARRIER_MANIFEST_CAPACITY {
bail!( bail!(
"Carrier-Manifest überschreitet die maximale Blockgröße ({} > {} Bytes).", "Carrier-Manifest überschreitet die maximale Blockgröße ({} > {} Bytes). Empfohlene Obergrenze ca. 7.000 Dateien/Verzeichnisse beachten.",
manifest_bytes.len(), manifest_bytes.len(),
crate::crypto::CHUNK_SIZE - 64 CARRIER_MANIFEST_CAPACITY
); );
} }
if manifest_bytes.len() > 800_000 { if manifest_bytes.len() >= CARRIER_MANIFEST_WARN_THRESHOLD {
let pct = (manifest_bytes.len() * 100) / CARRIER_MANIFEST_CAPACITY;
warn!( warn!(
"Carrier-Manifest erreicht 80% der Blockkapazität ({}/{} Bytes)", "Warnung (C-04): Hidden-Vault Manifest zu {}% gefüllt ({}/{} Bytes). Empfohlene Obergrenze beachten (~7.000 Dateien/Verzeichnisse).",
pct,
manifest_bytes.len(), manifest_bytes.len(),
crate::crypto::CHUNK_SIZE - 64 CARRIER_MANIFEST_CAPACITY
); );
} }
@@ -515,6 +528,19 @@ impl CarrierFs {
// Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Manifest-Kopie reservieren // Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Manifest-Kopie reservieren
manifest.free_blocks.retain(|&b| b >= 2); manifest.free_blocks.retain(|&b| b >= 2);
// C-04: Warnung bei Füllstand über 80% beim Mounten
if let Ok(manifest_bytes) = serde_json::to_vec(&manifest) {
if manifest_bytes.len() >= CARRIER_MANIFEST_WARN_THRESHOLD {
let pct = (manifest_bytes.len() * 100) / CARRIER_MANIFEST_CAPACITY;
warn!(
"Warnung (C-04): Hidden-Vault Manifest zu {}% gefüllt ({}/{} Bytes). Empfohlene Obergrenze beachten (~7.000 Dateien/Verzeichnisse).",
pct,
manifest_bytes.len(),
CARRIER_MANIFEST_CAPACITY
);
}
}
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.map(|d| d.as_secs()) .map(|d| d.as_secs())
@@ -581,6 +607,11 @@ impl CarrierFs {
inner.manifest_dirty inner.manifest_dirty
} }
pub fn manifest_usage(&self) -> (usize, usize, f64) {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.manifest_usage()
}
pub fn leak_counter(&self) -> Arc<AtomicU64> { pub fn leak_counter(&self) -> Arc<AtomicU64> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.leak_counter.clone() inner.leak_counter.clone()
+16
View File
@@ -422,6 +422,22 @@ pub async fn mount_container(
if enable_tray { if enable_tray {
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)"); println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
} }
if vault_id == 1 {
if let Some((len, cap, pct)) = fs_clone.carrier_manifest_usage() {
if len >= crate::carrier::CARRIER_MANIFEST_WARN_THRESHOLD {
println!(
" {} Warnung (C-04): Hidden-Vault Manifest zu {:.1}% gefüllt ({}/{} Bytes).",
ui::yellow("[!]"),
pct,
len,
cap
);
println!(
" Empfohlene Obergrenze beachten: ca. 7.000 Dateien/Verzeichnisse im Hidden Vault."
);
}
}
}
println!(); println!();
#[cfg(windows)] #[cfg(windows)]
println!( println!(
+4
View File
@@ -795,6 +795,10 @@ impl SanctumFs {
Ok(()) Ok(())
} }
pub fn carrier_manifest_usage(&self) -> Option<(usize, usize, f64)> {
self.carrier_fs.as_ref().map(|cfs| cfs.manifest_usage())
}
pub fn leak_count(&self) -> u64 { pub fn leak_count(&self) -> u64 {
self.leak_counter().load(Ordering::Relaxed) self.leak_counter().load(Ordering::Relaxed)
} }
+142
View File
@@ -1312,3 +1312,145 @@ async fn test_c03_manifest_dirty_decoupling_and_unmount_flush() {
drop(db); drop(db);
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
} }
#[tokio::test]
async fn test_c04_manifest_capacity_limit_and_warning() {
let path = temp_db_path("c04_capacity");
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
let carrier_name = "carrier_c04.bin";
let pass_decoy = "DecoyPass2026!";
let pass_hidden = "HiddenPass2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let salt_0 = generate_salt();
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
let dek_0 = generate_dek();
let salt_1 = generate_salt();
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
let dek_1 = generate_dek();
let carrier_node_id = 3i64;
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
let (wrapped_1, nonce_1, tag_1) =
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
let db = Database::open(&path).expect("Open database");
db.init_schema_with_carrier(
&salt_0,
&kdf_params,
&wrapped_0,
&nonce_0,
&tag_0,
Some((
carrier_name,
carrier_size_bytes,
&salt_1,
&kdf_params,
&wrapped_1,
&nonce_1,
&tag_1,
&dek_0,
&dek_1,
)),
)
.expect("Init carrier schema");
db.checkpoint().unwrap();
let meta = db.read_meta().unwrap();
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
let cfs = sanctum::carrier::CarrierFs::load(
db.clone(),
carrier_node_id,
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
std::sync::Arc::new(auth_hidden.dek().clone()),
auth_hidden.version(),
true,
)
.expect("CarrierFs load");
// 1. Initialer Zustand: Manifest minimal (< 5 KB, << 80%)
let (len, cap, pct) = cfs.manifest_usage();
assert_eq!(cap, sanctum::carrier::CARRIER_MANIFEST_CAPACITY);
assert!(len < 5_000);
assert!(pct < 1.0);
// 2. Fülle Manifest gezielt auf > 80% (> 838.809 Bytes)
// Erstelle Verzeichnisse mit langen Pfadnamen, bis die Größe über 840 KB liegt
for i in 0..1000 {
let dir_path = DavPath::new(&format!("/d_{:04}_{}", i, "a".repeat(1000))).unwrap();
cfs.create_dir(&dir_path).await.expect("create_dir");
let (current_len, _, _) = cfs.manifest_usage();
if current_len >= sanctum::carrier::CARRIER_MANIFEST_WARN_THRESHOLD {
break;
}
}
let (len80, _, pct80) = cfs.manifest_usage();
assert!(
len80 >= sanctum::carrier::CARRIER_MANIFEST_WARN_THRESHOLD,
"Manifest size must exceed 80% threshold, got: {}",
len80
);
assert!(
pct80 >= 80.0,
"Manifest pct must be >= 80.0%, got: {}",
pct80
);
// Speichern (sync_manifest) muss mit Warnung gelingen
cfs.sync_manifest()
.expect("sync_manifest at > 80% must succeed");
drop(cfs);
// Neu laden: muss erfolgreich laden und beim Laden die 80%-Warnung auslösen
let cfs_loaded = sanctum::carrier::CarrierFs::load(
db.clone(),
carrier_node_id,
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
std::sync::Arc::new(auth_hidden.dek().clone()),
auth_hidden.version(),
true,
)
.expect("CarrierFs load at > 80% must succeed");
let (reloaded_len, _, reloaded_pct) = cfs_loaded.manifest_usage();
assert!(reloaded_pct >= 80.0);
assert_eq!(reloaded_len, len80);
// 3. Grenze überschreiten: weiter füllen bis > 1.048.512 Bytes
for i in 1000..1500 {
let dir_path = DavPath::new(&format!("/overflow_{:04}_{}", i, "x".repeat(1000))).unwrap();
cfs_loaded
.create_dir(&dir_path)
.await
.expect("create_dir overflow");
let (cur_len, _, _) = cfs_loaded.manifest_usage();
if cur_len > sanctum::carrier::CARRIER_MANIFEST_CAPACITY {
break;
}
}
// sync_manifest() MUSS jetzt mit Fehler abbrechen
let err = cfs_loaded.sync_manifest().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("Carrier-Manifest überschreitet die maximale Blockgröße"),
"Error message must specify carrier capacity limit: {}",
err_str
);
assert!(
err_str.contains("7.000 Dateien/Verzeichnisse"),
"Error message must recommend file limit: {}",
err_str
);
drop(cfs_loaded);
drop(db);
let _ = std::fs::remove_file(&path);
}