fix(carrier): C-04 — document hidden vault capacity limit and add 80% mount warning
This commit is contained in:
@@ -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!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user