diff --git a/QUICKSTART.md b/QUICKSTART.md index d5464ab..8c39fbd 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -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`). 2. **Hidden-Passwort**: Öffnet den geheimen Zweittresor (Second Safe). * *(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! --- diff --git a/README.md b/README.md index 2baeb1a..8621cd9 100644 --- a/README.md +++ b/README.md @@ -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. - **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). + *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 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)**: diff --git a/src/carrier.rs b/src/carrier.rs index 05c6c2d..f35323d 100644 --- a/src/carrier.rs +++ b/src/carrier.rs @@ -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_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). #[derive(Debug, Clone, Serialize, Deserialize)] @@ -312,6 +314,15 @@ impl CarrierFsInner { 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<()> { self.manifest_save_count += 1; self.manifest_dirty = false; @@ -322,18 +333,20 @@ impl CarrierFsInner { .unwrap_or(0); self.manifest.manifest_generation += 1; 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!( - "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(), - 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!( - "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(), - 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 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() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) @@ -581,6 +607,11 @@ impl CarrierFs { 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 { let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); inner.leak_counter.clone() diff --git a/src/mount.rs b/src/mount.rs index 66178b4..e8a1ff2 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -422,6 +422,22 @@ pub async fn mount_container( if enable_tray { 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!(); #[cfg(windows)] println!( diff --git a/src/vfs.rs b/src/vfs.rs index 5d89cd9..52aa913 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -795,6 +795,10 @@ impl SanctumFs { 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 { self.leak_counter().load(Ordering::Relaxed) } diff --git a/tests/carrier_model_a_test.rs b/tests/carrier_model_a_test.rs index f561a4a..54518f8 100644 --- a/tests/carrier_model_a_test.rs +++ b/tests/carrier_model_a_test.rs @@ -1312,3 +1312,145 @@ async fn test_c03_manifest_dirty_decoupling_and_unmount_flush() { drop(db); 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); +}