fix(recovery): resolve metadata MAC desync after recovery (R-NEW-1) & complete release workflow (CI-01)
- 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
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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::*;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user