fix: address minor issues for v0.9.1 preparation (K-01 mount verification, path validation, legacy-names warning, V1-V2 migration docs)
This commit is contained in:
@@ -27,6 +27,7 @@ Dieses Release führt das **Carrier-Format V2** für den Hidden Vault ein. Es he
|
|||||||
- V1-Container werden vollkommen transparent eingelesen.
|
- V1-Container werden vollkommen transparent eingelesen.
|
||||||
- Beim ersten Speichervorgang (`save_manifest()`) wird geprüft, ob genügend freie Blöcke für die Seitenaufteilung vorhanden sind.
|
- Beim ersten Speichervorgang (`save_manifest()`) wird geprüft, ob genügend freie Blöcke für die Seitenaufteilung vorhanden sind.
|
||||||
- Reicht der Platz nicht aus, bricht der Vorgang sauber ab, ohne den Container zu beschädigen. Reicht der Platz, wird der Container nahtlos auf Format V2 migriert.
|
- Reicht der Platz nicht aus, bricht der Vorgang sauber ab, ohne den Container zu beschädigen. Reicht der Platz, wird der Container nahtlos auf Format V2 migriert.
|
||||||
|
- Eine ausführliche Schritt-für-Schritt-Anleitung zur Migration findet sich in der [README.md](README.md#10-migration-von-carrier-format-v1-auf-v2-hidden-vault).
|
||||||
- **D-04 & D-05: Deterministische Seitenaufteilung & Blockfreigabe**:
|
- **D-04 & D-05: Deterministische Seitenaufteilung & Blockfreigabe**:
|
||||||
- Deterministische Sortierung der Inodes nach ID garantiert reproduzierbare Seitenbelegungen.
|
- Deterministische Sortierung der Inodes nach ID garantiert reproduzierbare Seitenbelegungen.
|
||||||
- Dynamische Blockfreigabe: Verringert sich die Inode-Anzahl (z. B. durch Löschungen), werden überzählige Seitenblöcke mittels kryptografischem Zufallsrauschen geschreddert (`shred_carrier_block`) und an den Freispeicher zurückgegeben.
|
- Dynamische Blockfreigabe: Verringert sich die Inode-Anzahl (z. B. durch Löschungen), werden überzählige Seitenblöcke mittels kryptografischem Zufallsrauschen geschreddert (`shred_carrier_block`) und an den Freispeicher zurückgegeben.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Beim Erstellen eines Tresors wählst du zwischen zwei Sicherheitsstufen:
|
|||||||
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ät & Skalierung (Format V2)**: Dank Paged-Manifest-Architektur dynamisch skalierbar (~4.500–7.000 Dateien je 1-MB-Manifestseite, keine starre 7.000er-Grenze mehr). Sanctum warnt beim Mounten automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
|
* **Kapazität & Skalierung (Format V2)**: Dank Paged-Manifest-Architektur dynamisch skalierbar (~4.500–7.000 Dateien je 1-MB-Manifestseite, keine starre 7.000er-Grenze mehr). Sanctum warnt beim Mounten automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
|
||||||
|
* **Migration von V1-Containern**: Bestehende V1-Hidden-Vaults werden transparent unterstützt und können auf Format V2 migriert werden. Siehe [Schritt-für-Schritt-Anleitung in der README](README.md#10-migration-von-carrier-format-v1-auf-v2-hidden-vault).
|
||||||
* **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!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
|
|||||||
- **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).
|
||||||
*Carrier-Format V2 mit Paged Manifest*: Das steganografische Dateisystem des Hidden Vaults nutzt eine skalierbare Paged-Manifest-Architektur. Blöcke 0 und 1 speichern den redundanten Superblock (C-02), während Inodes über dedizierte Inode-Pages (~1 MB Nutzdaten je Seite, ca. 4.500–7.000 Inodes pro Seite) dynamisch aus dem Blockpool verwaltet werden. Die Kapazität ist nicht mehr auf 7.000 Dateien limitiert, sondern skaliert dynamisch mit den verfügbaren Trägerblöcken. Robuste Fail-Soft-Resilienz (D-01) isoliert Seitenbeschädigungen, ein In-Memory Sekundärindex (D-02) beschleunigt Pfadoperationen auf O(Geschwister), und Sanctum warnt beim Einbinden automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
|
*Carrier-Format V2 mit Paged Manifest*: Das steganografische Dateisystem des Hidden Vaults nutzt eine skalierbare Paged-Manifest-Architektur. Blöcke 0 und 1 speichern den redundanten Superblock (C-02), während Inodes über dedizierte Inode-Pages (~1 MB Nutzdaten je Seite, ca. 4.500–7.000 Inodes pro Seite) dynamisch aus dem Blockpool verwaltet werden. Die Kapazität ist nicht mehr auf 7.000 Dateien limitiert, sondern skaliert dynamisch mit den verfügbaren Trägerblöcken. Robuste Fail-Soft-Resilienz (D-01) isoliert Seitenbeschädigungen, ein In-Memory Sekundärindex (D-02) beschleunigt Pfadoperationen auf O(Geschwister), und Sanctum warnt beim Einbinden automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
|
||||||
- **Dateinamen-Verschlüsselung**:
|
- **Dateinamen-Verschlüsselung & Schutz vor Swap-Attacks**:
|
||||||
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 werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert, wobei die übergeordnete Verzeichnis-ID (`parent_id`) als Additional Authenticated Data (AAD) eingebunden ist. Dies verhindert, dass Angreifer verschlüsselte Dateinamen zwischen Verzeichnissen vertauschen können.
|
||||||
|
> [!WARNING]
|
||||||
|
> **Sicherheitshinweis zu `--legacy-names`**: Das globale Flag `--legacy-names` deaktiviert die Prüfung der AAD-Bindung bei der Entschlüsselung von Dateinamen. Verwenden Sie dieses Flag **ausschließlich zur einmaligen Datenrettung oder Migration** älterer Container (vor Version 0.8.0), da es das Schutzniveau gegen Manipulationen reduziert.
|
||||||
- **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**:
|
- **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**:
|
||||||
Vor jedem Löschen oder Kürzen werden Chunk-Payloads, Nonces und Tags in der SQLite-Datenbank transaktional mit CSPRNG-Zufallsrauschen überschrieben. Dies verhindert zuverlässig jede logische Rekonstruktion auf Datenbank- und Dateisystemebene.
|
Vor jedem Löschen oder Kürzen werden Chunk-Payloads, Nonces und Tags in der SQLite-Datenbank transaktional mit CSPRNG-Zufallsrauschen überschrieben. Dies verhindert zuverlässig jede logische Rekonstruktion auf Datenbank- und Dateisystemebene.
|
||||||
*Wichtiger technischer Hinweis (SA-07)*: Dies stellt ein *logisches* sicheres Löschen dar. Auf modernen Solid-State-Drives (SSD, NVMe) und Copy-on-Write-Dateisystemen (Btrfs, ZFS, APFS, ReFS) kann Software im Userland bauartbedingt keine *physikalische* Datenträgerbereinigung (Media Sanitization) garantieren: Der Flash Translation Layer (FTL) und Wear-Leveling-Algorithmen leiten Schreiboperationen auf neue Flash-Blöcke um; alte physikalische Zellen verbleiben bis zur SSD Garbage Collection / TRIM im Flash. Für absolute physische Bereinigung wird eine hardware- oder blockebenenbasierte Vollverschlüsselung (BitLocker, LUKS) oder ein ATA/NVMe Secure Erase empfohlen.
|
*Wichtiger technischer Hinweis (SA-07)*: Dies stellt ein *logisches* sicheres Löschen dar. Auf modernen Solid-State-Drives (SSD, NVMe) und Copy-on-Write-Dateisystemen (Btrfs, ZFS, APFS, ReFS) kann Software im Userland bauartbedingt keine *physikalische* Datenträgerbereinigung (Media Sanitization) garantieren: Der Flash Translation Layer (FTL) und Wear-Leveling-Algorithmen leiten Schreiboperationen auf neue Flash-Blöcke um; alte physikalische Zellen verbleiben bis zur SSD Garbage Collection / TRIM im Flash. Für absolute physische Bereinigung wird eine hardware- oder blockebenenbasierte Vollverschlüsselung (BitLocker, LUKS) oder ein ATA/NVMe Secure Erase empfohlen.
|
||||||
@@ -244,6 +246,46 @@ sanctum.exe upgrade --force
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 10. Migration von Carrier-Format V1 auf V2 (Hidden Vault)
|
||||||
|
|
||||||
|
Mit Sanctum v0.9.0 wurde das **Carrier-Format V2** eingeführt, das die vorherige Begrenzung von ca. 7.000 Dateien im Hidden Vault aufhebt. Bestehende V1-Container werden transparent unterstützt und können wie folgt auf V2 aktualisiert werden:
|
||||||
|
|
||||||
|
1. **Sicherheits-Backup erstellen**:
|
||||||
|
Erstellen Sie vor der Migration eine Sicherungskopie Ihres Containers:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe backup --path "C:\Pfad\tresor.sanctum" --output "C:\Pfad\tresor_backup.sanctum"
|
||||||
|
# Oder manuell:
|
||||||
|
Copy-Item "C:\Pfad\tresor.sanctum" "C:\Pfad\tresor_backup.sanctum"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Freispeicher prüfen**:
|
||||||
|
Für die Migration wird mindestens ein freier Trägerblock (~1 MB) für die erste Inode-Page benötigt. Bei extrem vollen Trägerdateien (0 freie Blöcke) bricht Sanctum die Migration sicher ab (`Out of space`), ohne den V1-Container zu verändern oder zu beschädigen.
|
||||||
|
|
||||||
|
3. **Hidden Vault einbinden (`mount`)**:
|
||||||
|
Öffnen Sie den Hidden Vault mit Ihrem Hidden-Passwort:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S
|
||||||
|
```
|
||||||
|
Sanctum erkennt automatisch das V1-Format, liest das bisherige Manifest ein und bereitet die V2-Struktur im Speicher vor.
|
||||||
|
|
||||||
|
4. **Migration durch Schreiboperation auslösen**:
|
||||||
|
Die Migration auf die V2-On-Disk-Struktur wird beim nächsten Speichervorgang des Manifests automatisch vollzogen. Sie können dies durch Anlegen und sofortiges Löschen einer temporären Datei anstoßen:
|
||||||
|
```powershell
|
||||||
|
New-Item "S:\.migrate" -ItemType File
|
||||||
|
Remove-Item "S:\.migrate"
|
||||||
|
sanctum.exe unmount --drive S
|
||||||
|
```
|
||||||
|
Beim Aushängen (`unmount`) wird das neue Paged-Manifest (V2-Superblock + Inode-Pages) atomar auf die Trägerdatei geschrieben.
|
||||||
|
|
||||||
|
5. **Integrität verifizieren**:
|
||||||
|
Überprüfen Sie den migrierten Container:
|
||||||
|
```powershell
|
||||||
|
sanctum.exe verify --path "C:\Pfad\tresor.sanctum"
|
||||||
|
```
|
||||||
|
Beim nächsten Mounten des Hidden Vaults zeigt Sanctum die dynamische Block- und Seitenstatistik des V2-Formats an.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
||||||
|
|
||||||
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die
|
|||||||
| **Slowloris DoS auf Loopback** | Ressourcenerschöpfung durch offengehaltene Sockets. | Beschränkung auf max. 64 gleichzeitige Verbindungen (`MAX_CONCURRENT_DAV_CONNECTIONS`) und 15s Header-Read-Timeout (`HTTP_HEADER_READ_TIMEOUT`). |
|
| **Slowloris DoS auf Loopback** | Ressourcenerschöpfung durch offengehaltene Sockets. | Beschränkung auf max. 64 gleichzeitige Verbindungen (`MAX_CONCURRENT_DAV_CONNECTIONS`) und 15s Header-Read-Timeout (`HTTP_HEADER_READ_TIMEOUT`). |
|
||||||
| **Begleitdateien-Forensik (-wal/-shm)** | Angreifer analysiert temporäre SQLite-Dateien während oder nach dem Mount. | Sämtliche Daten werden vor Übergabe an SQLite verschlüsselt; WAL/SHM enthalten ausschließlich Ciphertext; TRUNCATE-Checkpointing und Bereinigung beim Aushängen (Z-03). |
|
| **Begleitdateien-Forensik (-wal/-shm)** | Angreifer analysiert temporäre SQLite-Dateien während oder nach dem Mount. | Sämtliche Daten werden vor Übergabe an SQLite verschlüsselt; WAL/SHM enthalten ausschließlich Ciphertext; TRUNCATE-Checkpointing und Bereinigung beim Aushängen (Z-03). |
|
||||||
| **Nötigung / Schulterblick** | Zwang zur Passwortherausgabe. | Alibi-Carrier (Hidden Vault Modell A) mit steganografisch verstecktem zweiten Tresor und unabhängigen Schlüsseln. |
|
| **Nötigung / Schulterblick** | Zwang zur Passwortherausgabe. | Alibi-Carrier (Hidden Vault Modell A) mit steganografisch verstecktem zweiten Tresor und unabhängigen Schlüsseln. |
|
||||||
|
| **Dateinamen-Swap-Angriffe** | Angreifer vertauscht verschlüsselte Dateinamen zwischen Verzeichnissen. | AAD-Bindung an `parent_id` bei AES-256-GCM Verschlüsselung; Warnung vor `--legacy-names` (deaktiviert diesen Schutz). |
|
||||||
|
| **Path-Traversal & Gerätenamen** | Angreifer schleust Pfad-Traversal (`..`), reservierte Namen (`CON`, `PRN`) oder Null-Bytes ein. | Strikte Validierung (`validate_node_name`) in allen Schichten (`storage.rs`, `vfs.rs`, `carrier.rs`, `sync.rs`). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -126,3 +128,16 @@ Mit Sanctum v0.9.0 führt der Hidden Vault das Carrier-Format V2 (Paged Manifest
|
|||||||
- **Vorabprüfung bei Migration (D-03)**: Bei der transparenten Konvertierung alter V1-Manifeste nach V2 prüft Sanctum vorab, ob ausreichend freie Trägerblöcke vorhanden sind. Reicht der Speicherplatz nicht aus, wird der Speichervorgang ohne Beschädigung des V1-Containers abgebrochen.
|
- **Vorabprüfung bei Migration (D-03)**: Bei der transparenten Konvertierung alter V1-Manifeste nach V2 prüft Sanctum vorab, ob ausreichend freie Trägerblöcke vorhanden sind. Reicht der Speicherplatz nicht aus, wird der Speichervorgang ohne Beschädigung des V1-Containers abgebrochen.
|
||||||
- **Kryptografische Blockfreigabe (D-05)**: Werden Inodes gelöscht und Inode-Pages überflüssig, werden die freigegebenen Trägerblöcke sofort mit CSPRNG-Rauschen überschrieben (`shred_carrier_block`), bevor sie an den Freispeicher-Pool zurückgegeben werden.
|
- **Kryptografische Blockfreigabe (D-05)**: Werden Inodes gelöscht und Inode-Pages überflüssig, werden die freigegebenen Trägerblöcke sofort mit CSPRNG-Rauschen überschrieben (`shred_carrier_block`), bevor sie an den Freispeicher-Pool zurückgegeben werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dateinamen-Sicherheit und das `--legacy-names`-Flag
|
||||||
|
|
||||||
|
### 7.1 AAD-Bindung von Dateinamen
|
||||||
|
In Sanctum werden Dateinamen mit AES-256-GCM verschlüsselt. Um Swap-Angriffe zu verhindern (bei denen ein Angreifer mit Dateisystem-Zugriff verschlüsselte Dateinamen zwischen Verzeichnissen austauscht), wird die `parent_id` als zusätzliche authentifizierte Daten (AAD) an die Verschlüsselung gebunden.
|
||||||
|
|
||||||
|
### 7.2 Sicherheitsrisiko des `--legacy-names`-Flags
|
||||||
|
Frühere Versionen von Sanctum (vor v0.8.0) verwendeten leere AAD für Dateinamen. Das Flag `--legacy-names` erlaubt die Entschlüsselung von Dateinamen ohne AAD-Bindung als Fallback.
|
||||||
|
- **Risiko**: Bei aktiviertem `--legacy-names` ist der Schutz vor Vertauschen von Dateinamen aufgehoben. Ein Angreifer könnte Dateien manipulieren, indem er verschlüsselte Namen zwischen verschiedenen Verzeichnissen vertauscht.
|
||||||
|
- **Empfehlung**: Dieses Flag darf **ausschließlich** zur einmaligen Migration von Altdaten verwendet werden. Im regulären Betrieb muss es deaktiviert bleiben.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1007,6 +1007,9 @@ impl DavFileSystem for CarrierFs {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if options.create || options.create_new {
|
if options.create || options.create_new {
|
||||||
|
crate::pathutil::validate_node_name(file_name)
|
||||||
|
.map_err(|_| FsError::Forbidden)?;
|
||||||
|
|
||||||
let parent = inner.resolve_path(parent_path).ok_or(FsError::NotFound)?;
|
let parent = inner.resolve_path(parent_path).ok_or(FsError::NotFound)?;
|
||||||
if !parent.is_dir {
|
if !parent.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
@@ -1129,6 +1132,8 @@ impl DavFileSystem for CarrierFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate::pathutil::validate_node_name(dir_name).map_err(|_| FsError::Forbidden)?;
|
||||||
|
|
||||||
if inner.resolve_path(&path_str).is_some() {
|
if inner.resolve_path(&path_str).is_some() {
|
||||||
return Err(FsError::Exists);
|
return Err(FsError::Exists);
|
||||||
}
|
}
|
||||||
@@ -1248,6 +1253,8 @@ impl DavFileSystem for CarrierFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate::pathutil::validate_node_name(to_name).map_err(|_| FsError::Forbidden)?;
|
||||||
|
|
||||||
let to_parent = inner
|
let to_parent = inner
|
||||||
.resolve_path(to_parent_path)
|
.resolve_path(to_parent_path)
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
@@ -1324,6 +1331,8 @@ impl DavFileSystem for CarrierFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate::pathutil::validate_node_name(to_name).map_err(|_| FsError::Forbidden)?;
|
||||||
|
|
||||||
let to_parent = inner
|
let to_parent = inner
|
||||||
.resolve_path(to_parent_path)
|
.resolve_path(to_parent_path)
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|||||||
+3
-1
@@ -25,7 +25,7 @@ use sanctum::verify::verify_container;
|
|||||||
#[command(about = "Sanctum: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland", long_about = None)]
|
#[command(about = "Sanctum: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland", long_about = None)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Erlaubt Legacy-Namensentschlüsselung ohne deterministisches AAD (S-07)
|
/// SICHERHEITSHINWEIS: Unsicher! Deaktiviert die kryptografische Bindung (AAD) von Dateinamen (Gefahr von Swap-Attacks/Manipulation). Nur zur temporären Migration alter Container verwenden!
|
||||||
#[arg(long, global = true, default_value_t = false)]
|
#[arg(long, global = true, default_value_t = false)]
|
||||||
legacy_names: bool,
|
legacy_names: bool,
|
||||||
|
|
||||||
@@ -1591,6 +1591,8 @@ async fn run() -> Result<()> {
|
|||||||
|
|
||||||
if cli.legacy_names {
|
if cli.legacy_names {
|
||||||
set_allow_legacy_names(true);
|
set_allow_legacy_names(true);
|
||||||
|
eprintln!("WARNUNG: Das Flag '--legacy-names' ist aktiv. Die kryptografische Authentifizierung von Dateinamen ist geschwächt (Gefahr von Swap-Attacks). Verwenden Sie dieses Flag nur zur Datenmigration!");
|
||||||
|
tracing::warn!("Flag --legacy-names ist aktiv. Authenticated Associated Data (AAD) fuer Dateinamen ist abgeschwaecht.");
|
||||||
}
|
}
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
|
|||||||
@@ -187,6 +187,28 @@ pub async fn mount_container(
|
|||||||
// S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren
|
// S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren
|
||||||
let _advisory_lock = db.acquire_advisory_lock_guard(false)?;
|
let _advisory_lock = db.acquire_advisory_lock_guard(false)?;
|
||||||
|
|
||||||
|
// K-01: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3)
|
||||||
|
if version >= crate::crypto::FORMAT_VERSION_V3 {
|
||||||
|
match db.verify_metadata_mac_for_slot(vault_id, &dek) {
|
||||||
|
Ok(true) => {
|
||||||
|
if !stealth {
|
||||||
|
println!(
|
||||||
|
" • Metadaten-MAC: ✔ Integrität erfolgreich verifiziert (HMAC-SHA256)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
bail!(
|
||||||
|
"Metadaten-MAC-Verifikation fehlgeschlagen: Die Container-Metadaten wurden manipuliert oder sind beschädigt (K-01)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
bail!("Fehler bei der Metadaten-MAC-Verifikation: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.set_active_slot_and_dek(vault_id, dek.clone());
|
||||||
|
|
||||||
// 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren
|
// 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren
|
||||||
let mut token_bytes = [0u8; 16];
|
let mut token_bytes = [0u8; 16];
|
||||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
|
||||||
|
|||||||
@@ -1245,6 +1245,9 @@ impl DavFileSystem for SanctumFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_path_safety(to_name)?;
|
||||||
|
crate::pathutil::validate_node_name(to_name).map_err(|_| FsError::Forbidden)?;
|
||||||
|
|
||||||
let to_parent = self
|
let to_parent = self
|
||||||
.resolve_path(to_parent_path)?
|
.resolve_path(to_parent_path)?
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|||||||
@@ -510,9 +510,9 @@ async fn test_secondary_index_path_resolution_speed() {
|
|||||||
assert!(target.is_some());
|
assert!(target.is_some());
|
||||||
assert_eq!(target.unwrap().name, "child_4999.txt");
|
assert_eq!(target.unwrap().name, "child_4999.txt");
|
||||||
|
|
||||||
// Der Sekundärindex muss den Eintrag in unter 5 Millisekunden finden (typisch < 0.2 ms)
|
// Der Sekundärindex muss den Eintrag in unter 50 Millisekunden finden (typisch < 0.2 ms, O(1) statt O(N))
|
||||||
assert!(
|
assert!(
|
||||||
elapsed.as_millis() < 5,
|
elapsed.as_millis() < 50,
|
||||||
"Pfadauflösung dauerte zu lange: {:?}",
|
"Pfadauflösung dauerte zu lange: {:?}",
|
||||||
elapsed
|
elapsed
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -309,3 +309,85 @@ async fn test_z04_webdav_quota_report() {
|
|||||||
drop(db);
|
drop(db);
|
||||||
let _ = std::fs::remove_file(&container_path);
|
let _ = std::fs::remove_file(&container_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mount_rejects_tampered_metadata_mac() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let container_path: PathBuf = temp_dir.join(format!(
|
||||||
|
"test_mount_tampered_mac_{}.sanctum",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
if container_path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = "TestTamperedMacPassword2026!";
|
||||||
|
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(password, &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();
|
||||||
|
db.set_active_slot_and_dek(0, dek.clone());
|
||||||
|
|
||||||
|
// Erstelle einen Testknoten
|
||||||
|
let node = db.create_node(1, "original.txt", false).unwrap();
|
||||||
|
db.update_metadata_mac().unwrap();
|
||||||
|
|
||||||
|
// Vor Manipulation: verify_metadata_mac muss erfolgreich sein
|
||||||
|
assert!(db.verify_metadata_mac_for_slot(0, &dek).unwrap());
|
||||||
|
|
||||||
|
// Manipuliere SQLite-Metadaten direkt (Böswilliger Angreifer)
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE nodes SET name = 'hacked.exe' WHERE id = ?1",
|
||||||
|
[node.id],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach Manipulation: Direkte Verifikation meldet 'false' (Integritätsbruch)
|
||||||
|
assert!(!db.verify_metadata_mac_for_slot(0, &dek).unwrap());
|
||||||
|
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
// Aufruf von mount_container muss fail-closed abbrechen
|
||||||
|
let auth =
|
||||||
|
sanctum::mount::ContainerAuth::Password(zeroize::Zeroizing::new(password.to_string()));
|
||||||
|
let res = sanctum::mount::mount_container(
|
||||||
|
&container_path,
|
||||||
|
'Z',
|
||||||
|
None,
|
||||||
|
Some(18943),
|
||||||
|
auth,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"Mount mit manipulierten Metadaten muss fehlschlagen!"
|
||||||
|
);
|
||||||
|
let err_msg = res.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Metadaten-MAC-Verifikation fehlgeschlagen") || err_msg.contains("K-01"),
|
||||||
|
"Erwartete Fehlermeldung zu K-01 Metadaten-MAC, erhalten: {}",
|
||||||
|
err_msg
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&container_path);
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,3 +127,200 @@ fn test_storage_create_and_rename_reject_invalid_names() {
|
|||||||
|
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_carrier_fs_rejects_invalid_names() {
|
||||||
|
use dav_server::davpath::DavPath;
|
||||||
|
use dav_server::fs::{DavFileSystem, FsError, OpenOptions};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use sanctum::carrier::CarrierFs;
|
||||||
|
use sanctum::crypto::{
|
||||||
|
derive_kek, generate_dek, generate_salt, wrap_slot0_payload, wrap_slot1_payload, KdfParams,
|
||||||
|
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||||
|
};
|
||||||
|
use sanctum::storage::Database;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
let mut path = std::env::temp_dir();
|
||||||
|
let id: u64 = OsRng.next_u64();
|
||||||
|
path.push(format!("sanctum_test_carrier_pathval_{}.sanctum", id));
|
||||||
|
if path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let carrier_size_bytes = 10 * 1024 * 1024;
|
||||||
|
let carrier_name = "system_backup.dat";
|
||||||
|
let pass_decoy = "DecoyPassword2026!";
|
||||||
|
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
let cfs = CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
Arc::new(dek_0),
|
||||||
|
Arc::new(dek_1),
|
||||||
|
3,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("Load CarrierFs");
|
||||||
|
|
||||||
|
// 1. create_dir mit ungültigem Namen (Windows-Reservierung CON, PRN)
|
||||||
|
let p_con = DavPath::new("/CON").unwrap();
|
||||||
|
assert_eq!(cfs.create_dir(&p_con).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
let p_aux = DavPath::new("/aux.txt").unwrap();
|
||||||
|
assert_eq!(cfs.create_dir(&p_aux).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
// 2. open (create) mit ungültigem Namen
|
||||||
|
let p_nul = DavPath::new("/NUL").unwrap();
|
||||||
|
let mut opt_create = OpenOptions::default();
|
||||||
|
opt_create.create = true;
|
||||||
|
opt_create.write = true;
|
||||||
|
assert_eq!(
|
||||||
|
cfs.open(&p_nul, opt_create.clone()).await.err(),
|
||||||
|
Some(FsError::Forbidden)
|
||||||
|
);
|
||||||
|
|
||||||
|
let p_ctrl = DavPath::new("/prn.pdf").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cfs.open(&p_ctrl, opt_create.clone()).await.err(),
|
||||||
|
Some(FsError::Forbidden)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Gültige Datei erstellen
|
||||||
|
let p_valid = DavPath::new("/valid.txt").unwrap();
|
||||||
|
let file = cfs.open(&p_valid, opt_create).await;
|
||||||
|
assert!(file.is_ok(), "Valid file should be created");
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// 4. rename mit ungültigem Zielnamen
|
||||||
|
let p_com1 = DavPath::new("/COM1.txt").unwrap();
|
||||||
|
assert_eq!(cfs.rename(&p_valid, &p_com1).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
// 5. copy mit ungültigem Zielnamen
|
||||||
|
let p_lpt1 = DavPath::new("/LPT1").unwrap();
|
||||||
|
assert_eq!(cfs.copy(&p_valid, &p_lpt1).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_vfs_rejects_invalid_names() {
|
||||||
|
use dav_server::davpath::DavPath;
|
||||||
|
use dav_server::fs::{DavFileSystem, FsError, OpenOptions};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use sanctum::crypto::{
|
||||||
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION,
|
||||||
|
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||||
|
};
|
||||||
|
use sanctum::storage::Database;
|
||||||
|
use sanctum::vfs::SanctumFs;
|
||||||
|
|
||||||
|
let mut path = std::env::temp_dir();
|
||||||
|
let id: u64 = OsRng.next_u64();
|
||||||
|
path.push(format!("sanctum_test_vfs_pathval_{}.sanctum", id));
|
||||||
|
if path.exists() {
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = "TestPassword1234!";
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
|
let db = Database::open(&path).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
db.set_active_slot_and_dek(0, dek.clone());
|
||||||
|
|
||||||
|
let fs = SanctumFs::new(db.clone(), dek, FORMAT_VERSION);
|
||||||
|
|
||||||
|
// 1. create_dir mit ungültigem Namen
|
||||||
|
let p_con = DavPath::new("/CON").unwrap();
|
||||||
|
assert_eq!(fs.create_dir(&p_con).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
let p_aux = DavPath::new("/aux.txt").unwrap();
|
||||||
|
assert_eq!(fs.create_dir(&p_aux).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
// 2. open (create) mit ungültigem Namen
|
||||||
|
let p_nul = DavPath::new("/NUL").unwrap();
|
||||||
|
let mut opt_create = OpenOptions::default();
|
||||||
|
opt_create.create = true;
|
||||||
|
opt_create.write = true;
|
||||||
|
assert_eq!(
|
||||||
|
fs.open(&p_nul, opt_create.clone()).await.err(),
|
||||||
|
Some(FsError::Forbidden)
|
||||||
|
);
|
||||||
|
|
||||||
|
let p_prn = DavPath::new("/prn.pdf").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fs.open(&p_prn, opt_create.clone()).await.err(),
|
||||||
|
Some(FsError::Forbidden)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Gültige Datei erstellen
|
||||||
|
let p_valid = DavPath::new("/valid.txt").unwrap();
|
||||||
|
let file = fs.open(&p_valid, opt_create).await;
|
||||||
|
assert!(file.is_ok(), "Valid file should be created");
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// 4. rename mit ungültigem Zielnamen
|
||||||
|
let p_com1 = DavPath::new("/COM1.txt").unwrap();
|
||||||
|
assert_eq!(fs.rename(&p_valid, &p_com1).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
// 5. copy mit ungültigem Zielnamen
|
||||||
|
let p_lpt1 = DavPath::new("/LPT1").unwrap();
|
||||||
|
assert_eq!(fs.copy(&p_valid, &p_lpt1).await, Err(FsError::Forbidden));
|
||||||
|
|
||||||
|
drop(fs);
|
||||||
|
drop(db);
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user