1 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
16 changed files with 607 additions and 59 deletions
+82 -16
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()
$ElfHash = (Get-FileHash -Path "${DistDir}/sanctum" -Algorithm SHA256).Hash.ToLower()
@("$ZipHash ${PackageName}.zip", "$ExeHash sanctum.exe") | Set-Content -Path "${DistDir}/SHA256SUMS.txt" -Encoding utf8 @(
"$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
+23
View File
@@ -5,6 +5,29 @@ 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 ## [0.9.1] - 2026-09-20
### Sicherheits-Härtung, Pfadvalidierung & Migration-Dokumentation ### Sicherheits-Härtung, Pfadvalidierung & Migration-Dokumentation
Generated
+2 -1
View File
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "sanctum" name = "sanctum"
version = "0.9.1" 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.1" 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
+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
```
+17 -6
View File
@@ -1,17 +1,17 @@
# Sanctum Security Audit & Remediation Log (v0.9.1) # 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, und Abschnitt 6 die Nachbesserungen der Minor Issues aus v0.9.1. 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.1 (Basis v0.9.0) - **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:** 128 / 128 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) beim Mount 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), 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). - **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`) |
@@ -136,8 +137,18 @@ Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheits
- **Migration-Dokumentation (V1 → V2):** - **Migration-Dokumentation (V1 → V2):**
- Detaillierte 5-stufige Dokumentation für Anwender zur Konvertierung auf das Paged-Manifest-Format 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.
+1 -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.1** im reinen Userland-Betrieb. Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.9.2** im reinen Userland-Betrieb.
--- ---
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"version": "0.9.1", "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.1/sanctum-v0.9.1-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": "0409d5e67c173983693456620b217e9498dcdc1ae54a6bc67aa8e368ee3d0883", "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.1 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.1 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.1/sanctum-v0.9.1-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: 0409d5e67c173983693456620b217e9498dcdc1ae54a6bc67aa8e368ee3d0883 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
+14 -4
View File
@@ -187,17 +187,27 @@ 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) // K-01 / R-NEW-1: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3)
if version >= crate::crypto::FORMAT_VERSION_V3 { if version >= crate::crypto::FORMAT_VERSION_V3 {
match db.verify_metadata_mac_for_slot(vault_id, &dek) { match db.verify_metadata_mac_status_for_slot(vault_id, &dek) {
Ok(true) => { Ok(crate::storage::MetadataMacStatus::Valid) => {
if !stealth { if !stealth {
println!( println!(
" • Metadaten-MAC: ✔ Integrität erfolgreich verifiziert (HMAC-SHA256)" " • Metadaten-MAC: ✔ Integrität erfolgreich verifiziert (HMAC-SHA256)"
); );
} }
} }
Ok(false) => { 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!( bail!(
"Metadaten-MAC-Verifikation fehlgeschlagen: Die Container-Metadaten wurden manipuliert oder sind beschädigt (K-01)." "Metadaten-MAC-Verifikation fehlgeschlagen: Die Container-Metadaten wurden manipuliert oder sind beschädigt (K-01)."
); );
+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,
+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)
+289
View File
@@ -391,3 +391,292 @@ async fn test_mount_rejects_tampered_metadata_mac() {
let _ = std::fs::remove_file(&container_path); 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);
}