3 Commits
Author SHA1 Message Date
harald badbe3bd18 fix(recovery): resolve metadata MAC desync after recovery (R-NEW-1) & complete release workflow (CI-01)
Sanctum Release / Build & Test (Windows x86_64 & Linux musl) (push) Waiting to run
Sanctum Release / Sign & Release (push) Blocked by required conditions
- R-NEW-1: Recalculate metadata HMAC upon recovery-key restore and mark backup restores as PendingRebuild to rebuild transparently on first mount
- CI-01: Update release.yaml to compile both Windows x86_64 and Linux musl with pinned Zig 0.16.0 and cargo-zigbuild 0.23.4
- W-1: Implement statvfs quota determination on Unix via libc
- Add RELEASE_PROCESS.md documenting release architecture and steps
- Bump version to 0.9.2 across manifests, lockfile, docs, Scoop and WinGet
2026-09-21 08:15:17 +02:00
harald efb28c1c45 chore(release): bump version to 0.9.1 and update docs/manifests
Sanctum Release / Build & Test (Windows x86_64) (push) Waiting to run
Sanctum Release / Sign & Release (push) Blocked by required conditions
2026-09-20 17:31:03 +02:00
harald a252393f96 fix: address minor issues for v0.9.1 preparation (K-01 mount verification, path validation, legacy-names warning, V1-V2 migration docs) 2026-09-20 17:08:44 +02:00
23 changed files with 1013 additions and 62 deletions
+83 -17
View File
@@ -8,7 +8,7 @@ on:
jobs: jobs:
# SA-02 & SA-04: Entkoppelte Build- & Test-Umgebung ohne Zugriff auf Signatur-Secrets # SA-02 & SA-04: Entkoppelte Build- & Test-Umgebung ohne Zugriff auf Signatur-Secrets
build: build:
name: Build & Test (Windows x86_64) name: Build & Test (Windows x86_64 & Linux musl)
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout Code - name: Checkout Code
@@ -18,39 +18,101 @@ jobs:
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master commit pinned uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master commit pinned
with: with:
toolchain: "1.85.0" toolchain: "1.85.0"
targets: x86_64-pc-windows-msvc targets: x86_64-pc-windows-msvc,x86_64-unknown-linux-musl
- name: Install cargo-zigbuild (pinned)
run: cargo install cargo-zigbuild --locked --version 0.23.4
- name: Install Zig (pinned, hash-verified)
shell: pwsh
run: |
$ZigVersion = "0.16.0"
$ZigUrl = "https://ziglang.org/download/$ZigVersion/zig-windows-x86_64-$ZigVersion.zip"
$ExpectedZigHash = "68659eb5f1e4eb1437a722f1dd889c5a322c9954607f5edcf337bc3684a75a7e"
Invoke-WebRequest -Uri $ZigUrl -OutFile "zig.zip"
$ActualHash = (Get-FileHash -Path "zig.zip" -Algorithm SHA256).Hash.ToLower()
if ($ActualHash -ne $ExpectedZigHash) {
Write-Error "KRITISCHER SICHERHEITSFEHLER: Zig SHA-256 Pruefsumme ungueltig! Erwartet: $ExpectedZigHash, Erhalten: $ActualHash"
exit 1
}
Expand-Archive -Path "zig.zip" -DestinationPath "zig-bin"
echo "$PWD/zig-bin/zig-windows-x86_64-$ZigVersion" >> $env:GITHUB_PATH
- name: Run Tests - name: Run Tests
run: cargo test --all --verbose run: cargo test --all --verbose
- name: Build Release Binary - name: Build Windows Release Binary
run: cargo build --release run: cargo build --release
- name: Cross-build Linux musl binary
run: cargo-zigbuild zigbuild --target x86_64-unknown-linux-musl --release
- name: Package Artifacts - name: Package Artifacts
id: package id: package
shell: pwsh shell: pwsh
run: | run: |
$Tag = "${{ gitea.ref_name }}" $Tag = "${{ gitea.ref_name }}"
$DistDir = "dist" $DistDir = "dist"
$PackageName = "sanctum-${Tag}-windows-x86_64" if (-not (Test-Path $DistDir)) { New-Item -ItemType Directory -Path $DistDir -Force | Out-Null }
$StagingDir = "${DistDir}/${PackageName}"
$ZipFile = "${DistDir}/${PackageName}.zip"
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null # 1. Windows Package
Copy-Item "target/release/sanctum.exe" "$StagingDir/" $PackageNameWin = "sanctum-${Tag}-windows-x86_64"
Copy-Item "README.md" "$StagingDir/" $StagingDirWin = "${DistDir}/${PackageNameWin}"
Copy-Item "LICENSE" "$StagingDir/" $ZipFile = "${DistDir}/${PackageNameWin}.zip"
Copy-Item "CHANGELOG.md" "$StagingDir/"
Compress-Archive -Path "$StagingDir/*" -DestinationPath $ZipFile -Force New-Item -ItemType Directory -Path $StagingDirWin -Force | Out-Null
Copy-Item "target/release/sanctum.exe" "$StagingDirWin/"
Copy-Item "README.md" "$StagingDirWin/"
Copy-Item "LICENSE" "$StagingDirWin/"
Copy-Item "CHANGELOG.md" "$StagingDirWin/"
Copy-Item "QUICKSTART.md" "$StagingDirWin/"
Copy-Item "INSTALL.md" "$StagingDirWin/"
Copy-Item "LEGAL.md" "$StagingDirWin/"
Copy-Item "THIRD_PARTY_LICENSES.md" "$StagingDirWin/"
if (Test-Path "assets") { Copy-Item "assets" "$StagingDirWin/" -Recurse }
Compress-Archive -Path "$StagingDirWin/*" -DestinationPath $ZipFile -Force
Remove-Item $StagingDirWin -Recurse -Force
# Standalone Windows EXE nach dist/ kopieren
Copy-Item "target/release/sanctum.exe" "${DistDir}/sanctum.exe" -Force
# 2. Linux Package
$PackageNameLinux = "sanctum-${Tag}-linux-x86_64"
$StagingDirLinux = "${DistDir}/${PackageNameLinux}"
$TarGzFile = "${DistDir}/${PackageNameLinux}.tar.gz"
New-Item -ItemType Directory -Path $StagingDirLinux -Force | Out-Null
Copy-Item "target/x86_64-unknown-linux-musl/release/sanctum" "$StagingDirLinux/sanctum"
Copy-Item "README.md" "$StagingDirLinux/"
Copy-Item "LICENSE" "$StagingDirLinux/"
Copy-Item "CHANGELOG.md" "$StagingDirLinux/"
Copy-Item "QUICKSTART.md" "$StagingDirLinux/"
Copy-Item "INSTALL.md" "$StagingDirLinux/"
Copy-Item "LEGAL.md" "$StagingDirLinux/"
Copy-Item "THIRD_PARTY_LICENSES.md" "$StagingDirLinux/"
tar.exe -czf $TarGzFile -C $DistDir $PackageNameLinux
Remove-Item $StagingDirLinux -Recurse -Force
# Standalone Linux ELF nach dist/ kopieren
Copy-Item "target/x86_64-unknown-linux-musl/release/sanctum" "${DistDir}/sanctum" -Force
# 3. SHA-256 Checksums für alle 4 Assets
$ZipHash = (Get-FileHash -Path $ZipFile -Algorithm SHA256).Hash.ToLower() $ZipHash = (Get-FileHash -Path $ZipFile -Algorithm SHA256).Hash.ToLower()
$ExeHash = (Get-FileHash -Path "target/release/sanctum.exe" -Algorithm SHA256).Hash.ToLower() $ExeHash = (Get-FileHash -Path "${DistDir}/sanctum.exe" -Algorithm SHA256).Hash.ToLower()
$TarHash = (Get-FileHash -Path $TarGzFile -Algorithm SHA256).Hash.ToLower()
@("$ZipHash ${PackageName}.zip", "$ExeHash sanctum.exe") | Set-Content -Path "${DistDir}/SHA256SUMS.txt" -Encoding utf8 $ElfHash = (Get-FileHash -Path "${DistDir}/sanctum" -Algorithm SHA256).Hash.ToLower()
@(
"$ZipHash ${PackageNameWin}.zip",
"$ExeHash sanctum.exe",
"$TarHash ${PackageNameLinux}.tar.gz",
"$ElfHash sanctum"
) | Set-Content -Path "${DistDir}/SHA256SUMS.txt" -Encoding utf8
echo "ZIP_FILE=$ZipFile" >> $env:GITHUB_OUTPUT echo "ZIP_FILE=$ZipFile" >> $env:GITHUB_OUTPUT
echo "PACKAGE_NAME=$PackageName" >> $env:GITHUB_OUTPUT echo "PACKAGE_NAME=$PackageNameWin" >> $env:GITHUB_OUTPUT
- name: Upload Build Artifacts - name: Upload Build Artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
@@ -103,10 +165,11 @@ jobs:
$MinisignExe = "minisign" $MinisignExe = "minisign"
} }
$Tag = "${{ gitea.ref_name }}"
$KeyFile = "sanctum-ci-release.key" $KeyFile = "sanctum-ci-release.key"
[System.IO.File]::WriteAllText($KeyFile, $env:MINISIGN_SECRET_KEY) [System.IO.File]::WriteAllText($KeyFile, $env:MINISIGN_SECRET_KEY)
try { try {
& $MinisignExe -S -s $KeyFile -m "dist/SHA256SUMS.txt" -W -x "dist/SHA256SUMS.txt.minisig" & $MinisignExe -S -s $KeyFile -m "dist/SHA256SUMS.txt" -W -x "dist/SHA256SUMS.txt.minisig" -t "version:$Tag"
if ($LASTEXITCODE -ne 0 -or -not (Test-Path "dist/SHA256SUMS.txt.minisig")) { if ($LASTEXITCODE -ne 0 -or -not (Test-Path "dist/SHA256SUMS.txt.minisig")) {
Write-Error "Minisign-Signierung fehlgeschlagen!" Write-Error "Minisign-Signierung fehlgeschlagen!"
exit 1 exit 1
@@ -120,6 +183,9 @@ jobs:
with: with:
files: | files: |
dist/*.zip dist/*.zip
dist/*.tar.gz
dist/sanctum
dist/sanctum.exe
dist/SHA256SUMS.txt dist/SHA256SUMS.txt
dist/SHA256SUMS.txt.minisig dist/SHA256SUMS.txt.minisig
body_path: CHANGELOG.md body_path: CHANGELOG.md
+42
View File
@@ -5,6 +5,47 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/) Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/). und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
## [0.9.2] - 2026-09-21
### Recovery-MAC-Fix & Vollständiger Release-Workflow
Dieses Release behebt einen kritischen Fehler in den Disaster-Recovery-Pfaden (R-NEW-1), bei dem der K-01 Metadaten-MAC nach Header-Wiederherstellungen nicht aktualisiert wurde und nachfolgende Mount-Vorgänge fälschlich abwies. Zudem wird die CI/CD-Pipeline um den Linux-Cross-Build vervollständigt (CI-01) und die Quota-Ermittlung unter Linux implementiert (W-1).
#### Sicherheits- & Funktions-Fixes (R-NEW-1)
- **Metadaten-MAC nach Header-Wiederherstellung (R-NEW-1, HOCH)**:
- Bei Notfallwiederherstellung via 24-Wort BIP-39 Notfallschlüssel (`restore_slot_from_recovery_key`) wird der Metadaten-MAC sofort neu berechnet und persistent gespeichert.
- Bei Header-Wiederherstellung via `.sanctum.hdr`-Datei (`restore_header_backup`) wird der Zustand als `PendingRebuild` markiert und beim nächsten `sanctum mount` mit dem Benutzerpasswort transparent und sicher neu aufgebaut.
- Verhindert fälschliche Manipulations-Fehlermeldungen nach legitimen Notfall-Wiederherstellungen.
#### CI/CD & Build-Infrastruktur (CI-01)
- **Automatisierter Linux-Cross-Build in `.gitea/workflows/release.yaml`**:
- Vollständige Integration des Linux musl Cross-Builds (`x86_64-unknown-linux-musl`) auf dem Windows-Runner via `cargo-zigbuild` und SHA-256-gepinntem Zig-Compiler (`0.16.0`).
- Automatische Paketierung beider Plattformen (`.zip`, `.tar.gz`, Standalone-Binaries `sanctum.exe` und `sanctum`) und gemeinsame Minisign-Signatur in CI.
- Dokumentation des Release-Workflows in `RELEASE_PROCESS.md`.
#### Plattform-Optimierungen (W-1)
- **Freier Speicherplatz unter Linux (W-1)**:
- Ermittlung des freien Festplattenspeichers unter Linux/Unix via `libc::statvfs` (`f_bavail * f_frsize`) anstelle des statischen 1-TB-Platzhalters.
- **Plattform-Modul**:
- Präzisierung des Modul-Docstrings in `src/platform/mod.rs`.
## [0.9.1] - 2026-09-20
### Sicherheits-Härtung, Pfadvalidierung & Migration-Dokumentation
Dieses Release behebt die im Prüfbericht für v0.9.0 identifizierten vier Minor Issues bezüglich Metadaten-Authentifizierung beim Mounten, strikter Eingabevalidierung von Dateinamen im Carrier-Dateisystem, Dokumentation der Carrier-V1-nach-V2-Migration sowie Schutz vor Swap-Attacks bei der Dateinamen-Entschlüsselung.
#### Behobene Minor Issues
- **K-01 Metadaten-MAC-Validierung beim Mount**:
- `mount_container` validiert vor dem Start des WebDAV-Dienstes die kryptografische HMAC-SHA-256 Integrität aller Metadaten für Container ab Format V3.
- Registrierung des aktiven DEK (`set_active_slot_and_dek`), sodass fortlaufende Dateioperationen den Metadaten-MAC synchron halten.
- **Lückenlose Pfadvalidierung (`validate_node_name`)**:
- Konsequente Prüfung aller Dateinamen im Carrier-Dateisystem (`CarrierFs::open`, `CarrierFs::create_dir`, `CarrierFs::rename`, `CarrierFs::copy`) und VFS (`SanctumFs::copy`).
- Zuverlässige Abweisung von Windows-reservierten Gerätenamen (`CON`, `PRN`, `AUX`, `NUL`, `COM1-9`, `LPT1-9`), ungültigen Zeichen (`<>:"/\|?*`), Steuerzeichen und Pfad-Traversal.
- **Sicherheitsdokumentation & Runtime-Warnung für `--legacy-names`**:
- Deutliche Warnung im CLI-Hilfetext, auf `stderr` und im Logging vor den Risiken von Swap-Attacks bei deaktivierter AAD-Bindung.
- Aufnahme einer Risikobewertung in `README.md` und `THREAT_MODEL.md` (Abschnitt 7).
- **Migration-Leitfaden (V1 → V2)**:
- Schritt-für-Schritt-Anleitung in `README.md` (Abschnitt 10) zur transparenten Aktualisierung bestehender V1-Container auf das Paged-Manifest-Format V2.
## [0.9.0] - 2026-09-20 ## [0.9.0] - 2026-09-20
### Paged Manifest für den Hidden Vault (Aufhebung der Kapazitätsgrenze) ### Paged Manifest für den Hidden Vault (Aufhebung der Kapazitätsgrenze)
@@ -27,6 +68,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.
Generated
+2 -1
View File
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "sanctum" name = "sanctum"
version = "0.9.0" version = "0.9.2"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
@@ -1473,6 +1473,7 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-util", "hyper-util",
"libc",
"lz4_flex", "lz4_flex",
"minisign-verify", "minisign-verify",
"rand", "rand",
+4 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "sanctum" name = "sanctum"
version = "0.9.0" version = "0.9.2"
edition = "2021" edition = "2021"
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"] authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV" description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
@@ -48,6 +48,9 @@ tempfile = "3"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
tray-item = "0.10" tray-item = "0.10"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
lto = true lto = true
+1
View File
@@ -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.5007.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.5007.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!
--- ---
+44 -2
View File
@@ -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.5007.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.5007.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:
+62
View File
@@ -0,0 +1,62 @@
# Sanctum Release-Prozess & CI/CD Dokumentation
Dieses Dokument beschreibt den Release-Workflow von **Sanctum**, die Absicherung der Build-Pipeline (SA-02, SA-03, SA-04) und die unterstützten Veröffentlichungswege.
---
## 1. Automatisierter CI/CD-Workflow (Primärer Release-Pfad)
Ab Version **0.9.2** bildet der CI-Workflow unter [`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml) den gesamten Release-Prozess für alle Zielplattformen vollautomatisch auf dem `windows-latest`-Runner ab:
### Pipeline-Architektur:
1. **Trigger**:
- Push eines Git-Release-Tags nach dem Schema `v*` (z. B. `v0.9.2`).
2. **Build & Test Job** (`build`):
- **Plattform**: `windows-latest`.
- **Toolchains**:
- Rust Toolchain gepinnt (`1.85.0`).
- Rust Targets: `x86_64-pc-windows-msvc` und `x86_64-unknown-linux-musl`.
- `cargo-zigbuild` gepinnt auf `0.23.4`.
- `Zig` Compiler gepinnt auf `0.16.0` mit zwingender SHA-256-Integritätsprüfung (`68659eb5f1e4eb1437a722f1dd889c5a322c9954607f5edcf337bc3684a75a7e`).
- **Verifikation**:
- Vollständige Ausführung der Testsuite (`cargo test --all --verbose`).
- **Kompilierung**:
- Nativer Windows x86_64 Release-Build mit LTO (`cargo build --release`).
- Statischer Linux musl Cross-Build (`cargo-zigbuild zigbuild --target x86_64-unknown-linux-musl --release`).
- **Paketierung**:
- Erstellung von `sanctum-v<Version>-windows-x86_64.zip` (inkl. `sanctum.exe`, Dokumentation und Assets).
- Erstellung von `sanctum-v<Version>-linux-x86_64.tar.gz` (inkl. `sanctum` und Dokumentation).
- Bereitstellung der eigenständigen Binärdateien `sanctum.exe` und `sanctum`.
- Berechnung und Erstellung der vierzeiligen Prüfsummendatei `SHA256SUMS.txt`.
3. **Sign & Release Job** (`sign-and-release`):
- **Geheimnis-Isolation (SA-02)**: Das Signiergeheimnis `MINISIGN_SECRET_KEY` ist ausschließlich in diesem isolierten Job verfügbar, nicht in der Build- oder Testumgebung.
- **Signierung**:
- Gepinntes `minisign.exe` (SHA-256 geprüft).
- Signiert `SHA256SUMS.txt` mit `trusted comment: version:<Version>`.
- **Gitea Release**:
- Veröffentlicht das Release unter `https://gitea.pansi.eu/harald/sanctum/releases/tag/v<Version>` mit allen 6 Assets:
- `sanctum-v<Version>-windows-x86_64.zip`
- `sanctum.exe`
- `sanctum-v<Version>-linux-x86_64.tar.gz`
- `sanctum`
- `SHA256SUMS.txt`
- `SHA256SUMS.txt.minisig`
---
## 2. Lokaler Fallback-Prozess (Manuell)
Sollte die CI-Infrastruktur nicht verfügbar sein, kann der Release-Prozess lokal über PowerShell repliziert werden:
1. **Windows-Paket erstellen**:
```powershell
powershell -ExecutionPolicy Bypass -File scripts/package-release.ps1
```
2. **Linux-Paket erstellen (Cross-Build)**:
```powershell
powershell -ExecutionPolicy Bypass -File scripts/package-release-linux.ps1
```
3. **Auf Gitea veröffentlichen**:
```powershell
powershell -ExecutionPolicy Bypass -File scripts/publish-release.ps1
```
+30 -8
View File
@@ -1,17 +1,17 @@
# Sanctum Security Audit & Remediation Log (v0.9.0) # Sanctum Security Audit & Remediation Log (v0.9.2)
Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheitsaudits zusammen, dokumentiert die angewandten Härtungsmaßnahmen, referenziert die jeweiligen Git-Commits und benennt die zugehörigen automatisierten Regressionstests. Abschnitt 5 dokumentiert die Formaterweiterung Carrier V2 (Paged Manifest) aus v0.9.0. Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheitsaudits sowie nachfolgende Härtungen zusammen, dokumentiert die angewandten Härtungsmaßnahmen, referenziert die jeweiligen Git-Commits und benennt die zugehörigen automatisierten Regressionstests. Abschnitt 5 dokumentiert die Formaterweiterung Carrier V2 (Paged Manifest) aus v0.9.0, Abschnitt 6 die Minor Issues aus v0.9.1 und Abschnitt 7 den Recovery-MAC-Fix (R-NEW-1) aus v0.9.2.
--- ---
## Audit-Zusammenfassung ## Audit-Zusammenfassung
- **Zielversion:** Sanctum v0.9.0 (Basis v0.8.1 Audit-Abschluss) - **Zielversion:** Sanctum v0.9.2 (Basis v0.9.1)
- **Behobene Findings:** 39 / 39 (100%) - **Behobene Findings:** 39 / 39 (100%) Audit-Findings + R-NEW-1
- **Test-Ergebnis:** 130 / 130 Tests erfolgreich (100% Pass Rate)
- **Quality Gates:** `cargo fmt --check` (100% sauber), `cargo clippy --all-targets -- -D warnings` (0 Warnungen) - **Quality Gates:** `cargo fmt --check` (100% sauber), `cargo clippy --all-targets -- -D warnings` (0 Warnungen)
- **Container-Format:** Container-Format V3 mit kanonischer Metadaten-Authentifizierung (HMAC-SHA256) und Chunk-Replay-Schutz (Generation-gebundenes AAD). - **Container-Format:** Container-Format V3 mit kanonischer Metadaten-Authentifizierung (HMAC-SHA256) beim Mount und Chunk-Replay-Schutz (Generation-gebundenes AAD).
- **Hidden Vault & Storage-Resilienz:** Carrier-Format V2 (Paged Manifest), Dual-Block rollierender Superblock (C-02), Manifest-Entkopplung mit Dirty-Tracking (C-03), Fail-Soft Inode-Isolation (D-01), Sekundärindex (D-02), dynamische Kapazitätsskalierung und Blockwarnung, Fail-Closed Node-Name-Decryption (ST-01), rekursive Fehleraggregation (ST-02) und atomare SQLite-Transaktionen für Baum-Löschungen (ST-03). - **Hidden Vault & Storage-Resilienz:** Carrier-Format V2 (Paged Manifest), Dual-Block rollierender Superblock (C-02), Manifest-Entkopplung mit Dirty-Tracking (C-03), Fail-Soft Inode-Isolation (D-01), Sekundärindex (D-02), lückenlose Pfadvalidierung (`validate_node_name`), dynamische Kapazitätsskalierung und Blockwarnung, Fail-Closed Node-Name-Decryption (ST-01), rekursive Fehleraggregation (ST-02) und atomare SQLite-Transaktionen für Baum-Löschungen (ST-03).
- **Disaster Recovery Resilienz (R-NEW-1):** Vollständige Aktualisierung und Rebuild-Fähigkeit des Metadaten-MAC nach Wiederherstellung via Notfallschlüssel oder Header-Backup.
--- ---
@@ -19,6 +19,7 @@ Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheits
| Finding | Priorität | Modul | Kurzbeschreibung | Commit | Status & Regressionstest | | Finding | Priorität | Modul | Kurzbeschreibung | Commit | Status & Regressionstest |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| **R-NEW-1** | HOCH | `recovery`, `storage`, `mount` | Metadaten-MAC nach Header-Wiederherstellung: direkte Neuberechnung bei Recovery-Key und PendingRebuild-Status bei Backup-Restore | `v0.9.2` | ✅ Bestanden (`test_mount_succeeds_and_rebuilds_mac_after_header_file_restore`, `test_mount_succeeds_after_recovery_key_restore_slot0`, `test_mount_succeeds_after_recovery_key_restore_slot1`) |
| **C-02** | HOCH | `carrier` | Single Point of Failure beseitigt: Rollierendes Dual-Block-Manifest (Block 0 & 1, `manifest_generation`) mit transparenter Failover-Wiederherstellung | `8455dc0` | ✅ Bestanden (`test_c02_dual_block_rolling_manifest_redundancy_and_recovery`) | | **C-02** | HOCH | `carrier` | Single Point of Failure beseitigt: Rollierendes Dual-Block-Manifest (Block 0 & 1, `manifest_generation`) mit transparenter Failover-Wiederherstellung | `8455dc0` | ✅ Bestanden (`test_c02_dual_block_rolling_manifest_redundancy_and_recovery`) |
| **C-03** | MITTEL | `carrier`, `vfs`, `mount` | Entkopplung der Manifest-Neuverschlüsselung via Dirty-Tracking; synchrone Sicherung bei File-Flush und Unmount | `c606fa8` | ✅ Bestanden (`test_c03_manifest_dirty_decoupling_and_unmount_flush`) | | **C-03** | MITTEL | `carrier`, `vfs`, `mount` | Entkopplung der Manifest-Neuverschlüsselung via Dirty-Tracking; synchrone Sicherung bei File-Flush und Unmount | `c606fa8` | ✅ Bestanden (`test_c03_manifest_dirty_decoupling_and_unmount_flush`) |
| **C-04** | NIEDRIG | `carrier`, `mount`, `doc` | Dokumentation der Hidden-Vault-Kapazität (~7.000 Inodes) & automatische 80%-Füllstandswarnung beim Mounten | `c21d63a` | ✅ Bestanden (`test_c04_manifest_capacity_limit_and_warning`) | | **C-04** | NIEDRIG | `carrier`, `mount`, `doc` | Dokumentation der Hidden-Vault-Kapazität (~7.000 Inodes) & automatische 80%-Füllstandswarnung beim Mounten | `c21d63a` | ✅ Bestanden (`test_c04_manifest_capacity_limit_and_warning`) |
@@ -125,8 +126,29 @@ Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheits
- Ersetzung der statischen 80%-Füllstandswarnung durch eine Block-Verfügbarkeitswarnung bei `< 20` freien Blöcken oder `< 5%` Restkapazität. - Ersetzung der statischen 80%-Füllstandswarnung durch eine Block-Verfügbarkeitswarnung bei `< 20` freien Blöcken oder `< 5%` Restkapazität.
- **Automatisierte Testabdeckung:** 7 neue Regressionstests in `tests/carrier_v2_paged_manifest_test.rs` decken Kapazität (>10.000 Inodes), Migration, Fail-Soft-Isolation, Sekundärindex-Konsistenz, Pfadauflösungs-Performance, Out-of-Space-Abbruch und Blockfreigabe ab (Gesamttests: 130/130 erfolgreich). - **Automatisierte Testabdeckung:** 7 neue Regressionstests in `tests/carrier_v2_paged_manifest_test.rs` decken Kapazität (>10.000 Inodes), Migration, Fail-Soft-Isolation, Sekundärindex-Konsistenz, Pfadauflösungs-Performance, Out-of-Space-Abbruch und Blockfreigabe ab (Gesamttests: 130/130 erfolgreich).
### 6. Sanctum v0.9.1 — Sicherheits-Härtung, Pfadvalidierung & Migration-Dokumentation
- **Metadaten-MAC-Validierung beim Mount (K-01):**
- `mount_container` validiert vor dem Bereitstellen des Dateisystems die Integrität der SQLite-Metadaten via `db.verify_metadata_mac_for_slot`.
- Bei Manipulation bricht der Vorgang fail-closed ab; nachfolgende VFS-Operationen führen den MAC über den aktiven DEK fort.
- **Lückenlose Pfadvalidierung (`validate_node_name`):**
- Carrier-Dateisystem (`CarrierFs::open`, `CarrierFs::create_dir`, `CarrierFs::rename`, `CarrierFs::copy`) und VFS (`SanctumFs::copy`) validieren Knotennamen lückenlos gegen Windows-Reservierungen (`CON`, `PRN`, `AUX`, `NUL`, etc.) und verbotene Steuerzeichen.
- **Dokumentation & Runtime-Warnung zu `--legacy-names`:**
- Schutz vor Swap-Attacks bei der Dateinamen-Entschlüsselung; explizite Warnung im CLI-Hilfetext, auf stderr und im Logging bei Aktivierung.
- **Migration-Dokumentation (V1 → V2):**
- Detaillierte 5-stufige Dokumentation für Anwender zur Konvertierung auf das Paged-Manifest-Format V2.
### 7. Sanctum v0.9.2 — Recovery-MAC-Fix (R-NEW-1) & CI/CD-Vervollständigung (CI-01)
- **Metadaten-MAC nach Header-Wiederherstellung (R-NEW-1):**
- Behebt die Interaktionslücke zwischen dem K-01 Metadaten-MAC und den Disaster-Recovery-Pfaden.
- `restore_slot_from_recovery_key` aktualisiert den MAC sofort (`db.set_active_slot_and_dek` + `db.update_metadata_mac()`).
- `restore_header_backup` setzt `metadata_gen = 0` und `metadata_mac = NULL`. `verify_metadata_mac_status_for_slot` liefert `MetadataMacStatus::PendingRebuild`, wodurch `mount_container` den MAC beim ersten Einbinden transparent und sicher neu aufbaut.
- **Vollständiger Linux-Cross-Build in CI (CI-01):**
- `.gitea/workflows/release.yaml` kompiliert und paketiert Windows x86_64 und Linux musl auf demselben Runner.
- **Freier Speicherplatz unter Linux (W-1):**
- `get_available_disk_space` nutzt unter Unix `libc::statvfs` zur exakten Quota-Ermittlung (`f_bavail * f_frsize`).
--- ---
## Verifikationsnachweis ## Verifikationsnachweis
Alle Unit- und Integrationstests wurden auf einem Windows x86_64 Host mit 100% Erfolgsquote ausgeführt (130 / 130 Tests). Die Release-Paketierung für Windows und Linux musl ist voll automatisiert und wird mit Minisign kryptografisch abgesichert. Alle Unit- und Integrationstests wurden auf einem Windows x86_64 Host mit 100% Erfolgsquote ausgeführt. Die Release-Paketierung für Windows und Linux musl ist voll automatisiert und wird mit Minisign kryptografisch abgesichert.
+16 -1
View File
@@ -1,6 +1,6 @@
# Threat Model & Sicherheitsarchitektur von Sanctum # Threat Model & Sicherheitsarchitektur von Sanctum
Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.9.0** im reinen Userland-Betrieb. Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.9.2** im reinen Userland-Betrieb.
--- ---
@@ -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.
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"version": "0.9.0", "version": "0.9.2",
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV", "description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
"homepage": "https://gitea.pansi.eu/harald/sanctum", "homepage": "https://gitea.pansi.eu/harald/sanctum",
"license": "MIT", "license": "MIT",
"architecture": { "architecture": {
"64bit": { "64bit": {
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.0/sanctum-v0.9.0-windows-x86_64.zip", "url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.2/sanctum-v0.9.2-windows-x86_64.zip",
"hash": "47e26713880b812a332f7ee09737ace2ca3d7471d6375c0febe2d9b040ee8b43", "hash": "5828fca32b7d0581b740ef4f3cfb21c3717f92ee80d2e3f62e953a1ef083ac18",
"bin": "sanctum.exe" "bin": "sanctum.exe"
} }
}, },
+4 -4
View File
@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json # yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
PackageIdentifier: HaraldPansi.Sanctum PackageIdentifier: HaraldPansi.Sanctum
PackageVersion: 0.9.0 PackageVersion: 0.9.2
PackageName: Sanctum PackageName: Sanctum
Publisher: Harald Pansi Publisher: Harald Pansi
PublisherUrl: https://gitea.pansi.eu/harald PublisherUrl: https://gitea.pansi.eu/harald
@@ -18,7 +18,7 @@ Tags:
- webdav - webdav
- container - container
- plausible-deniability - plausible-deniability
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.0 ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.2
Installers: Installers:
- Architecture: x64 - Architecture: x64
InstallerType: zip InstallerType: zip
@@ -26,7 +26,7 @@ Installers:
NestedInstallerFiles: NestedInstallerFiles:
- RelativeFilePath: sanctum.exe - RelativeFilePath: sanctum.exe
PortableCommandAlias: sanctum PortableCommandAlias: sanctum
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.0/sanctum-v0.9.0-windows-x86_64.zip InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.2/sanctum-v0.9.2-windows-x86_64.zip
InstallerSha256: 47e26713880b812a332f7ee09737ace2ca3d7471d6375c0febe2d9b040ee8b43 InstallerSha256: 5828fca32b7d0581b740ef4f3cfb21c3717f92ee80d2e3f62e953a1ef083ac18
ManifestType: singleton ManifestType: singleton
ManifestVersion: 1.6.0 ManifestVersion: 1.6.0
+1 -1
View File
@@ -85,7 +85,7 @@ if (-not $Release) {
$PayloadPath = Join-Path $DistDir "release_req.json" $PayloadPath = Join-Path $DistDir "release_req.json"
$Payload = @{ $Payload = @{
tag_name = $TagName tag_name = $TagName
name = "Sanctum $TagName (Windows x86_64)" name = "Sanctum $TagName (Windows & Linux x86_64)"
body = $Changelog body = $Changelog
draft = $false draft = $false
prerelease = $false prerelease = $false
+9
View File
@@ -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
View File
@@ -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 {
+32
View File
@@ -187,6 +187,38 @@ 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 / R-NEW-1: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3)
if version >= crate::crypto::FORMAT_VERSION_V3 {
match db.verify_metadata_mac_status_for_slot(vault_id, &dek) {
Ok(crate::storage::MetadataMacStatus::Valid) => {
if !stealth {
println!(
" • Metadaten-MAC: ✔ Integrität erfolgreich verifiziert (HMAC-SHA256)"
);
}
}
Ok(crate::storage::MetadataMacStatus::PendingRebuild) => {
if !stealth {
println!(
" • Metadaten-MAC: Metadaten-MAC nach Wiederherstellung neu aufgebaut"
);
}
db.set_active_slot_and_dek(vault_id, dek.clone());
db.update_metadata_mac()?;
db.checkpoint()?;
}
Ok(crate::storage::MetadataMacStatus::Invalid) => {
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);
+3 -3
View File
@@ -1,7 +1,7 @@
//! Plattform-Abstraktionsschicht für Sanctum. //! Plattform-Abstraktionsschicht für Sanctum.
//! //!
//! Dieses Modul bündelt alle betriebssystemspezifischen Funktionen //! Re-Exportiert die plattformspezifischen Implementierungen (Speichersperren,
//! (Speichersperren, Dateimanager-Aufrufe, Signal-Monitoring, Shell-Integration) //! Prozessüberwachung, Session-Lock, Plattenplatzprüfung und Shell-Integration)
//! für Windows, Linux und macOS unter einer einheitlichen, speichersicheren Schnittstelle. //! aus `crate::windows` (welches via `#[cfg]`-Gates Windows-, Linux- und POSIX-Code bereitstellt).
pub use crate::windows::*; pub use crate::windows::*;
+28
View File
@@ -412,6 +412,11 @@ pub fn restore_slot_from_recovery_key(
db.restore_meta(&meta) db.restore_meta(&meta)
.context("Fehler beim Schreiben des rekonstruierten Headers")?; .context("Fehler beim Schreiben des rekonstruierten Headers")?;
// R-NEW-1: Metadaten-MAC für den wiederhergestellten Slot unmittelbar berechnen und persistieren
db.set_active_slot_and_dek(target_slot_id, dek.clone());
db.update_metadata_mac()
.context("Fehler beim Aktualisieren des Metadaten-MAC nach Header-Rekonstruktion")?;
db.checkpoint() db.checkpoint()
.context("Fehler beim WAL-Checkpoint nach Header-Rekonstruktion")?; .context("Fehler beim WAL-Checkpoint nach Header-Rekonstruktion")?;
@@ -561,6 +566,16 @@ mod tests {
.expect("Unwrap restored DEK"); .expect("Unwrap restored DEK");
assert_eq!(*dek, *active_dek); assert_eq!(*dek, *active_dek);
// R-NEW-1: Nach restore_header_backup muss der MAC-Status PendingRebuild sein
let mac_status = restored_db
.verify_metadata_mac_status_for_slot(0, &active_dek)
.expect("Verify MAC status");
assert_eq!(
mac_status,
crate::storage::MetadataMacStatus::PendingRebuild,
"Nach Header-Restore ohne Plaintext-DEK muss MAC-Status PendingRebuild sein"
);
let _ = fs::remove_file(&container_path); let _ = fs::remove_file(&container_path);
let _ = fs::remove_file(&backup_path); let _ = fs::remove_file(&backup_path);
} }
@@ -618,6 +633,19 @@ mod tests {
.expect("Unwrap rescued DEK"); .expect("Unwrap rescued DEK");
assert_eq!(*dek, *unwrapped); assert_eq!(*dek, *unwrapped);
// R-NEW-1: Nach recovery key restore muss der MAC sofort gültig und persistent sein
let mac_status = rescued_db
.verify_metadata_mac_status_for_slot(0, &unwrapped)
.expect("Verify MAC status");
assert_eq!(
mac_status,
crate::storage::MetadataMacStatus::Valid,
"Nach Recovery-Key-Restore muss MAC sofort Valid sein"
);
assert!(rescued_db
.verify_metadata_mac_for_slot(0, &unwrapped)
.unwrap());
let _ = fs::remove_file(&container_path); let _ = fs::remove_file(&container_path);
} }
} }
+47 -17
View File
@@ -208,6 +208,18 @@ fn current_timestamp() -> u64 {
.unwrap_or(0) .unwrap_or(0)
} }
/// Status der Metadaten-MAC-Integritätsprüfung (K-01 / R-NEW-1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataMacStatus {
/// Metadaten-MAC ist vorhanden und stimmt mit den kanonischen Metadaten überein.
Valid,
/// Container-Header wurde frisch aus einem Backup restauriert (metadata_gen == 0 && metadata_mac IS NULL).
/// Der MAC muss beim Mounten transparent mit dem aktiven DEK neu aufgebaut werden.
PendingRebuild,
/// Metadaten-MAC fehlt (bei gen > 0) oder stimmt nicht mit den berechneten Daten überein (Manipulationsverdacht).
Invalid,
}
impl Database { impl Database {
/// Öffnet oder erstellt die Container-Datenbank und initialisiert die Pragmas. /// Öffnet oder erstellt die Container-Datenbank und initialisiert die Pragmas.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> { pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
@@ -2316,6 +2328,15 @@ impl Database {
/// Prüft die Integrität des Metadaten-MAC für einen spezifischen Slot (0: Decoy, 1: Hidden). /// Prüft die Integrität des Metadaten-MAC für einen spezifischen Slot (0: Decoy, 1: Hidden).
pub fn verify_metadata_mac_for_slot(&self, slot_id: u32, dek: &[u8; 32]) -> Result<bool> { pub fn verify_metadata_mac_for_slot(&self, slot_id: u32, dek: &[u8; 32]) -> Result<bool> {
Ok(self.verify_metadata_mac_status_for_slot(slot_id, dek)? == MetadataMacStatus::Valid)
}
/// Prüft den detaillierten Integritätsstatus des Metadaten-MAC für einen spezifischen Slot (R-NEW-1).
pub fn verify_metadata_mac_status_for_slot(
&self,
slot_id: u32,
dek: &[u8; 32],
) -> Result<MetadataMacStatus> {
let conn = self.conn(); let conn = self.conn();
let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn
.query_row( .query_row(
@@ -2326,19 +2347,24 @@ impl Database {
.optional()?; .optional()?;
let Some((version, mac_opt, gen)) = meta_row else { let Some((version, mac_opt, gen)) = meta_row else {
return Ok(false); return Ok(MetadataMacStatus::Invalid);
}; };
if version < FORMAT_VERSION_V3 { if version < FORMAT_VERSION_V3 {
return Ok(true); return Ok(MetadataMacStatus::Valid);
}
// R-NEW-1: Frisch restaurierter Container (gen == 0 && mac_opt IS NULL)
if gen == 0 && mac_opt.is_none() {
return Ok(MetadataMacStatus::PendingRebuild);
} }
let Some(mac_bytes) = mac_opt else { let Some(mac_bytes) = mac_opt else {
return Ok(false); return Ok(MetadataMacStatus::Invalid);
}; };
if mac_bytes.len() != 32 { if mac_bytes.len() != 32 {
return Ok(false); return Ok(MetadataMacStatus::Invalid);
} }
let mut expected_mac = [0u8; 32]; let mut expected_mac = [0u8; 32];
@@ -2347,12 +2373,11 @@ impl Database {
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?; let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
let mac_key = derive_metadata_mac_key(dek); let mac_key = derive_metadata_mac_key(dek);
Ok(verify_metadata_mac( if verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac) {
&mac_key, Ok(MetadataMacStatus::Valid)
gen, } else {
&canonical, Ok(MetadataMacStatus::Invalid)
&expected_mac, }
))
} }
/// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01 & K-02). /// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01 & K-02).
@@ -2528,7 +2553,12 @@ impl Database {
kdf_params TEXT NOT NULL, kdf_params TEXT NOT NULL,
wrapped_dek BLOB NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_nonce BLOB NOT NULL,
header_tag BLOB NOT NULL header_tag BLOB NOT NULL,
metadata_mac BLOB,
metadata_gen INTEGER NOT NULL DEFAULT 0,
lock_pid INTEGER,
lock_host TEXT,
lock_time INTEGER
);", );",
)?; )?;
@@ -2542,8 +2572,8 @@ impl Database {
} }
let params_json = serde_json::to_string(&slot.kdf_params)?; let params_json = serde_json::to_string(&slot.kdf_params)?;
conn.execute( conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag) "INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, 0)",
params![ params![
slot.slot_id, slot.slot_id,
MAGIC_BYTES.as_slice(), MAGIC_BYTES.as_slice(),
@@ -2559,8 +2589,8 @@ impl Database {
} else { } else {
let params_json = serde_json::to_string(&meta.kdf_params)?; let params_json = serde_json::to_string(&meta.kdf_params)?;
conn.execute( conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag) "INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen)
VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7)", VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0)",
params![ params![
MAGIC_BYTES.as_slice(), MAGIC_BYTES.as_slice(),
meta.version, meta.version,
@@ -2578,8 +2608,8 @@ impl Database {
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot(); let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
let dummy_params_json = serde_json::to_string(&KdfParams::default())?; let dummy_params_json = serde_json::to_string(&KdfParams::default())?;
conn.execute( conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag) "INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen)
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7)", VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0)",
params![ params![
MAGIC_BYTES.as_slice(), MAGIC_BYTES.as_slice(),
FORMAT_VERSION, FORMAT_VERSION,
+3
View File
@@ -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)?;
+26 -1
View File
@@ -890,7 +890,32 @@ pub fn get_available_disk_space<P: AsRef<std::path::Path>>(path: P) -> Option<u6
None None
} }
} }
#[cfg(not(windows))] #[cfg(unix)]
{
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let p = path.as_ref();
let dir = if p.is_dir() {
p.to_path_buf()
} else {
p.parent()
.map(|parent| parent.to_path_buf())
.unwrap_or_else(|| p.to_path_buf())
};
let Ok(c_path) = CString::new(dir.as_os_str().as_bytes()) else {
return None;
};
unsafe {
let mut stat: libc::statvfs = std::mem::zeroed();
if libc::statvfs(c_path.as_ptr(), &mut stat) == 0 {
let free = (stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64);
Some(free)
} else {
None
}
}
}
#[cfg(not(any(windows, unix)))]
{ {
let _ = path; let _ = path;
Some(1024 * 1024 * 1024 * 1024) Some(1024 * 1024 * 1024 * 1024)
+2 -2
View File
@@ -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
); );
+371
View File
@@ -309,3 +309,374 @@ 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);
}
#[tokio::test]
async fn test_mount_succeeds_and_rebuilds_mac_after_header_file_restore() {
use sanctum::recovery::{export_header_backup, restore_header_backup};
use sanctum::storage::MetadataMacStatus;
let temp_dir = std::env::temp_dir();
let id = std::process::id();
let container_path: PathBuf = temp_dir.join(format!("test_hdr_restore_mount_{}.sanctum", id));
let backup_path: PathBuf = temp_dir.join(format!("test_hdr_restore_mount_{}.sanctum.hdr", id));
if container_path.exists() {
let _ = std::fs::remove_file(&container_path);
}
if backup_path.exists() {
let _ = std::fs::remove_file(&backup_path);
}
let password = "RestoreHeaderPassword2026!";
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());
let _ = db.create_node(1, "important_doc.pdf", false).unwrap();
db.update_metadata_mac().unwrap();
db.checkpoint().unwrap();
drop(db);
// 1. Header-Backup exportieren
export_header_backup(&container_path, &backup_path).expect("Export header");
// 2. Header mutwillig zerstören
{
let conn = rusqlite::Connection::open(&container_path).unwrap();
conn.execute("DELETE FROM meta", []).unwrap();
}
// 3. Header aus Backup wiederherstellen
restore_header_backup(&container_path, &backup_path).expect("Restore header");
// 4. Status vor dem Mount prüfen: muss PendingRebuild sein
{
let check_db = Database::open(&container_path).unwrap();
let status = check_db
.verify_metadata_mac_status_for_slot(0, &dek)
.unwrap();
assert_eq!(
status,
MetadataMacStatus::PendingRebuild,
"Nach restore_header_backup muss Status PendingRebuild sein"
);
}
// 5. Echter Mount-Aufruf muss gelingen und den MAC transparent neu aufbauen
let auth =
sanctum::mount::ContainerAuth::Password(zeroize::Zeroizing::new(password.to_string()));
let c_path = container_path.clone();
let mount_task = tokio::spawn(async move {
sanctum::mount::mount_container(
&c_path,
'Y',
None,
Some(19482),
auth,
false,
false,
None,
false,
false,
None,
true,
)
.await
});
// Kurz warten, bis mount_container die Vorabprüfung & den MAC-Rebuild vollzogen hat
tokio::time::sleep(tokio::time::Duration::from_millis(250)).await;
mount_task.abort();
// 6. Nach dem Mount: MAC muss nun Valid und persistent gespeichert sein!
let verified_db = Database::open(&container_path).unwrap();
let new_status = verified_db
.verify_metadata_mac_status_for_slot(0, &dek)
.unwrap();
assert_eq!(
new_status,
MetadataMacStatus::Valid,
"Nach Mount muss der MAC erfolgreich neu aufgebaut und Valid sein"
);
assert!(verified_db.verify_metadata_mac_for_slot(0, &dek).unwrap());
let _ = std::fs::remove_file(&container_path);
let _ = std::fs::remove_file(&backup_path);
}
#[tokio::test]
async fn test_mount_succeeds_after_recovery_key_restore_slot0() {
use sanctum::crypto::dek_to_mnemonic;
use sanctum::recovery::restore_header_from_recovery_key;
use sanctum::storage::MetadataMacStatus;
let temp_dir = std::env::temp_dir();
let id = std::process::id();
let container_path: PathBuf = temp_dir.join(format!("test_rec_mount_slot0_{}.sanctum", id));
if container_path.exists() {
let _ = std::fs::remove_file(&container_path);
}
let old_password = "ForgottenOldPassword2026!";
let new_password = "RescuedNewPassword2026!";
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(old_password, &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
let phrase = dek_to_mnemonic(&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());
let _ = db.create_node(1, "my_secrets.txt", false).unwrap();
db.update_metadata_mac().unwrap();
db.checkpoint().unwrap();
drop(db);
// Header zerstören
{
let conn = rusqlite::Connection::open(&container_path).unwrap();
conn.execute("DELETE FROM meta", []).unwrap();
}
// Mit 24-Wort Notfallschlüssel und neuem Passwort wiederherstellen
restore_header_from_recovery_key(&container_path, &phrase, new_password)
.expect("Restore from recovery key");
// R-NEW-1: Unmittelbar nach restore_from_recovery_key MUSS der MAC Valid sein!
let restored_db = Database::open(&container_path).unwrap();
let status = restored_db
.verify_metadata_mac_status_for_slot(0, &dek)
.unwrap();
assert_eq!(
status,
MetadataMacStatus::Valid,
"Nach restore_from_recovery_key muss MAC sofort Valid sein"
);
assert!(
restored_db.verify_metadata_mac_for_slot(0, &dek).unwrap(),
"verify_metadata_mac_for_slot muss direkt Ok(true) liefern (R-NEW-1 Fix)"
);
drop(restored_db);
// Mount mit neuem Passwort ausführen
let auth =
sanctum::mount::ContainerAuth::Password(zeroize::Zeroizing::new(new_password.to_string()));
let c_path = container_path.clone();
let mount_task = tokio::spawn(async move {
sanctum::mount::mount_container(
&c_path,
'Y',
None,
Some(19483),
auth,
false,
false,
None,
false,
false,
None,
true,
)
.await
});
tokio::time::sleep(tokio::time::Duration::from_millis(250)).await;
mount_task.abort();
let _ = std::fs::remove_file(&container_path);
}
#[tokio::test]
async fn test_mount_succeeds_after_recovery_key_restore_slot1() {
use rand::RngCore;
use sanctum::crypto::{dek_to_mnemonic, wrap_slot0_payload, wrap_slot1_payload};
use sanctum::recovery::restore_slot_from_recovery_key;
use sanctum::storage::MetadataMacStatus;
let temp_dir = std::env::temp_dir();
let id: u64 = rand::rngs::OsRng.next_u64();
let container_path: PathBuf = temp_dir.join(format!("test_rec_mount_slot1_{}.sanctum", id));
if container_path.exists() {
let _ = std::fs::remove_file(&container_path);
}
let pass_decoy = "DecoyPassword2026!";
let pass_hidden_old = "HiddenPasswordOld2026!";
let pass_hidden_new = "HiddenPasswordNew2026!";
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_old, &salt_1, &kdf_params).unwrap();
let dek_1 = generate_dek();
let phrase_1 = dek_to_mnemonic(&dek_1).unwrap();
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(&container_path).unwrap();
db.init_schema_with_carrier(
&salt_0,
&kdf_params,
&wrapped_0,
&nonce_0,
&tag_0,
Some((
"carrier.dat",
10 * 1024 * 1024,
&salt_1,
&kdf_params,
&wrapped_1,
&nonce_1,
&tag_1,
&dek_0,
&dek_1,
)),
)
.unwrap();
db.set_active_slot_and_dek(1, dek_1.clone());
db.update_metadata_mac().unwrap();
db.checkpoint().unwrap();
drop(db);
// Slot 1 Header zerstören
{
let conn = rusqlite::Connection::open(&container_path).unwrap();
conn.execute("DELETE FROM meta WHERE slot_id = 1", [])
.unwrap();
}
// Slot 1 mit 24-Wort Schlüssel und neuem Passwort wiederherstellen (dek_0 vorhanden)
restore_slot_from_recovery_key(&container_path, &phrase_1, pass_hidden_new, 1, Some(&dek_0))
.expect("Restore slot 1 from recovery key");
// R-NEW-1: Unmittelbar nach restore_slot_from_recovery_key MUSS Slot 1 MAC Valid sein!
let restored_db = Database::open(&container_path).unwrap();
let status_1 = restored_db
.verify_metadata_mac_status_for_slot(1, &dek_1)
.unwrap();
assert_eq!(
status_1,
MetadataMacStatus::Valid,
"Nach restore_slot_from_recovery_key muss Slot 1 MAC Valid sein"
);
assert!(
restored_db.verify_metadata_mac_for_slot(1, &dek_1).unwrap(),
"verify_metadata_mac_for_slot(1) muss direkt Ok(true) liefern (R-NEW-1 Fix)"
);
drop(restored_db);
let _ = std::fs::remove_file(&container_path);
}
+197
View File
@@ -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);
}