8 Commits
Author SHA1 Message Date
harald 9d1a4d3ad7 build: preserve dist directory and merge SHA256 sums in package-release.ps1
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
2026-09-14 13:36:59 +02:00
harald aa25719c0d fix(linux): prevent premature unmount caused by closed channel triggers in select!
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
2026-09-14 13:27:10 +02:00
harald 34378998c4 feat(linux): add --mount-point CLI option and platform-aware mount messages
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
2026-09-12 14:36:43 +02:00
harald 4ef0c414fa feat(linux): add static musl x86_64 build, packaging, and Freedesktop integration
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
2026-09-10 17:45:04 +02:00
harald 1f3afb1e52 fix(scripts): correct URL variable interpolation and add error check in publish script
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
2026-09-10 17:05:15 +02:00
harald 5f2040f8bf chore(release): bump version to 0.4.0 and update manifests
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
2026-09-10 17:02:05 +02:00
harald da2ddbd6ab feat(distribution): add Scoop/Winget manifests, INSTALL.md, and Unix mount integration 2026-09-10 16:51:24 +02:00
harald 46f976b353 feat(cross-platform): add QUICKSTART.md, live crash test, and platform abstraction module 2026-09-10 16:31:57 +02:00
17 changed files with 1117 additions and 223 deletions
+25 -4
View File
@@ -5,6 +5,31 @@ 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.4.0] - 2026-09-10
### Added
- **Distribution & Package Ecosystem**:
- Offizielles Scoop-Manifest (`packages/scoop/sanctum.json`) für One-Click CLI-Installation und nahtlose Upgrades unter Windows.
- Windows Package Manager (Winget) Manifest (`packages/winget/HaraldPansi.Sanctum.yaml`) zur systemweiten Paketverwaltung.
- Vollständiger Installations-Leitfaden (`INSTALL.md`) für Scoop, Winget, Standalone-Binaries und `cargo install`.
- **Cross-Platform Architektur (Linux & macOS Vorbereitung)**:
- Neues Plattform-Abstraktionsmodul `src/platform/mod.rs` für OS-unabhängiges Speicherschutz- und Mount-Handling.
- POSIX-Memory-Hardening mit `mlock` und `munlock` Systemaufrufen für Unix-Systeme.
- Plattformübergreifendes Datei- und Ordneröffnen (`explorer.exe` unter Windows, `xdg-open` unter Linux, `open` unter macOS).
- Native WebDAV-VFS-Einbindung für Linux GNOME/KDE via `gio mount`.
- **Chaos Engineering & Live-Crash-Resilienz**:
- `tests/live_crash_resilience_test.rs`: Umfassende automatisierte Stresstests mit Multi-Worker Schreibabbrüchen, asynchronen Panic-Simulationen und Validierung der SQLite WAL-Konsistenz (Null Korruption).
- **Legal, Compliance & Software Bill of Materials (SBOM)**:
- `LEGAL.md`: Rechtliche Leitlinien, US EAR § 742.15(b) & EU Dual-Use Exportkontroll-Klassifizierung, DSGVO Art. 25 Zero-Data-Erklärung und Haftungsausschluss nach § 521 BGB.
- `THIRD_PARTY_LICENSES.md`: Lückenloses Lizenzaudit aller 231 Abhängigkeiten (100% permissive Lizenzen, 0% Copyleft) zur strikten Compliance mit Apache-2.0 Section 4.
- Aufnahme von `LEGAL.md` und `THIRD_PARTY_LICENSES.md` in das offizielle Release-Distributionsarchiv (`package-release.ps1`).
- **Statisches Linux Single-Binary (`x86_64-unknown-linux-musl`)**:
- 100% statisch gelinktes Linux-Binary ohne GLIBC-Abhängigkeiten via Zig-Cross-Toolchain.
- Neues Linux-Distributionsarchiv `sanctum-v0.4.0-linux-x86_64.tar.gz` inklusive Dokumentation und Prüfsummen.
- Native Freedesktop `.desktop`-Datei und MIME-Type-Integration (`application/x-sanctum`) via `sanctum register`.
- **OpSec & Disaster Recovery UX**:
- `QUICKSTART.md`: Druckbare 24-Wörter BIP-39 Notfallkarte zur sicheren analogen Verwahrung des Notfallschlüssels (Air-Gapped Vault Paper Backup).
## [0.3.1] - 2026-09-10 ## [0.3.1] - 2026-09-10
### Added ### Added
@@ -20,10 +45,6 @@ und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semve
- **DFIR & Windows Host-Memory Hardening**: - **DFIR & Windows Host-Memory Hardening**:
- Win32 `VirtualLock` Speicherschutz: Physische Verriegelung des Root-DEK im RAM zur Unterbindung von Auslagerungen in `pagefile.sys` oder `swapfile.sys`. - Win32 `VirtualLock` Speicherschutz: Physische Verriegelung des Root-DEK im RAM zur Unterbindung von Auslagerungen in `pagefile.sys` oder `swapfile.sys`.
- Windows Defender Controlled Folder Access (CFA / Ransomware-Schutz) Diagnose mit gezielter Hilfestellung. - Windows Defender Controlled Folder Access (CFA / Ransomware-Schutz) Diagnose mit gezielter Hilfestellung.
- **Legal, Compliance & Export-Control (LEGAL-01 bis LEGAL-03)**:
- `LEGAL.md`: Umfassende rechtliche Dokumentation inkl. Exportkontroll-Klassifizierung (US EAR § 742.15(b) & EU Dual-Use GSN), Aufklärung über Plausible Deniability bei Key-Disclosure-Gesetzen, EU/DACH-Haftungsbeschränkung gem. § 521 BGB (Schenkungsrecht) sowie DSGVO Art. 25 Zero-Data-Erklärung.
- `THIRD_PARTY_LICENSES.md`: Lückenlose Attribution aller ~230 statisch eingebundenen Crates (Apache-2.0, MIT, BSD, SQLite Public Domain) zur strikten Erfüllung von Section 4 der Apache-2.0-Lizenz.
- Automatisierte Aufnahme von `LEGAL.md` und `THIRD_PARTY_LICENSES.md` in das Release-Packaging (`package-release.ps1`).
### Changed ### Changed
- **Visuelle Parität & Plausible Deniability beim Mounten (HF-01)**: - **Visuelle Parität & Plausible Deniability beim Mounten (HF-01)**:
Generated
+1 -1
View File
@@ -1377,7 +1377,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "sanctum" name = "sanctum"
version = "0.3.1" version = "0.4.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
+3 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "sanctum" name = "sanctum"
version = "0.3.1" version = "0.4.0"
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"
@@ -36,6 +36,8 @@ dyn-clone = "1.0"
lz4_flex = "0.11" lz4_flex = "0.11"
bip39 = { version = "2.2", features = ["zeroize"] } bip39 = { version = "2.2", features = ["zeroize"] }
hex = "0.4" hex = "0.4"
[target.'cfg(windows)'.dependencies]
tray-item = "0.10" tray-item = "0.10"
[profile.release] [profile.release]
+94
View File
@@ -0,0 +1,94 @@
# Installation & Paketmanager-Guide für Sanctum 🛡️
Sanctum kann auf verschiedene Arten unter Windows 10/11 installiert und verwaltet werden. Es sind **keine Administratorrechte** erforderlich.
---
## 🚀 1. Installation via Scoop (Empfohlen für Entwickler)
Wenn du den Windows-Paketmanager [Scoop](https://scoop.sh/) nutzt, kannst du Sanctum direkt über das bereitgestellte Manifest installieren:
```powershell
# Direkt über die URL des Manifests installieren:
scoop install https://gitea.pansi.eu/harald/sanctum/raw/branch/main/packages/scoop/sanctum.json
```
Oder falls du ein lokales Repository geklont hast:
```powershell
scoop install .\packages\scoop\sanctum.json
```
Nach der Installation ist der Befehl `sanctum` sofort global in deiner PowerShell verfügbar.
---
## 🪟 2. Installation via Windows Package Manager (Winget)
Sanctum verfügt über ein standardkonformes Winget-Manifest (Schema 1.6.0).
### Lokale Installation via Manifest:
```powershell
winget install --manifest .\packages\winget\HaraldPansi.Sanctum.yaml
```
Winget entpackt das portable Binary automatisch in das Benutzer-Anwendungsverzeichnis und verknüpft das Alias `sanctum`.
---
## 📦 3. Manuelle Installation (Standalone Portable)
1. Lade das aktuelle Release-Paket herunter:
* **URL:** [https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.4.0](https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.4.0)
* **Datei:** `sanctum-v0.4.0-windows-x86_64.zip`
2. Entpacke das Archiv in ein Verzeichnis deiner Wahl (z. B. `C:\Tools\Sanctum\`).
3. *(Optional)* Registriere das Windows Explorer Rechtsklick-Kontextmenü:
```powershell
.\sanctum.exe register
```
*(Erlaubt das Einhängen per Rechtsklick "In Sanctum öffnen" ohne Adminrechte).*
---
## 🐧 4. Installation unter Linux (x86_64)
Sanctum wird für Linux als **100 % statisches Single-Binary** (`x86_64-unknown-linux-musl`) ohne jegliche GLIBC- oder Bibliotheksabhängigkeiten bereitgestellt. Es läuft direkt auf Ubuntu, Debian, Alpine, Arch, Fedora und weiteren Distributionen:
1. Lade das Linux-Release herunter und installiere es:
```bash
curl -LO https://gitea.pansi.eu/harald/sanctum/releases/download/v0.4.0/sanctum-v0.4.0-linux-x86_64.tar.gz
tar -xzf sanctum-v0.4.0-linux-x86_64.tar.gz
chmod +x sanctum
sudo install -m 755 sanctum /usr/local/bin/sanctum
```
2. *(Optional)* Freedesktop-Integration einrichten (Doppelklick in GNOME / KDE / Thunar):
```bash
sanctum register
```
*Richtet die `.desktop`-Datei und den MIME-Typ `application/x-sanctum` im Userland (`~/.local/share`) ein.*
---
## 🔄 Updates
* **Scoop:**
```powershell
scoop update sanctum
```
* **Winget:**
```powershell
winget upgrade --manifest .\packages\winget\HaraldPansi.Sanctum.yaml
```
---
## 🗑️ Deinstallation
* **Scoop:**
```powershell
scoop uninstall sanctum
```
* **Explorer-Kontextmenü rückstandslos bereinigen:**
```powershell
sanctum.exe unregister
```
+113
View File
@@ -0,0 +1,113 @@
# Sanctum 🛡️ — Schnellstartanleitung & Notfallhandbuch
Willkommen bei **Sanctum**! Diese Anleitung führt dich in wenigen Minuten durch die grundlegenden Funktionen zur sicheren Verwaltung deiner verschlüsselten Container unter Windows 10/11 **100% Userland, ohne Administratorrechte und ohne Treiber**.
---
## ⚡ In 3 Minuten startklar
```powershell
# 1. Neuen Tresor mit Notfallkarte anlegen:
sanctum.exe init --path "C:\Users\DeinName\Dokumente\mein_tresor.sanctum"
# 2. Tresor einbinden (wählt automatisch den nächsten freien Buchstaben, z. B. Z:):
sanctum.exe mount --path "C:\Users\DeinName\Dokumente\mein_tresor.sanctum"
# 3. Sicher trennen:
# Entweder Ctrl+C im Konsolenfenster ODER Rechtsklick auf das Schild-Icon im Infobereich (Systray) -> "Aushängen & Beenden".
```
---
## 📂 Die Kernfunktionen im Überblick
### 1. Tresor initialisieren (`init`)
Beim Erstellen eines Tresors wählst du zwischen zwei Sicherheitsstufen:
* **Standard-Tresor (Single-Vault)**:
```powershell
sanctum.exe init --path "D:\Tresor\daten.sanctum"
```
* **Plausible Deniability Tresor (Dual-Vault mit Alibi-Carrier)**:
```powershell
sanctum.exe init --path "D:\Tresor\daten.sanctum" --with-hidden
```
* Hier legst du **zwei verschiedene Passwörter** fest:
1. **Decoy-Passwort**: Öffnet den äußeren Safe (enthält eine scheinbare Backup-Datei `system_backup.dat`).
2. **Hidden-Passwort**: Öffnet den geheimen, unnachweisbaren Safe.
* **Wichtig**: Notiere dir die ausgegebenen **24 Wörter des Notfallschlüssels (BIP-39)** auf der untenstehenden Notfallkarte!
---
### 2. Tresor einbinden (`mount`)
Sanctum erkennt automatisch anhand des eingegebenen Passworts, ob der Decoy- oder Hidden-Safe geöffnet werden soll.
| Befehl | Zweck |
| :--- | :--- |
| `sanctum mount --path <DATEI>` | Standard-Mount. Öffnet das Laufwerk automatisch im Explorer. |
| `sanctum mount --path <DATEI> --drive S` | Bindet den Tresor fest an den Buchstaben `S:` (statt automatischer Wahl). |
| `sanctum mount --path <DATEI> --idle-timeout 300` | Trennt das Laufwerk automatisch nach 5 Minuten (300 Sek.) Inaktivität. |
| `sanctum mount --path <DATEI> --no-open` | Verhindert das automatische Öffnen des Windows Explorers (Schutz vor ShellBag-Spuren). |
| `sanctum mount --path <DATEI> --stealth` | **Lautloser Stealth-Modus**: Keine Terminal-Ausgaben, keine URLs, kein Explorer-Start. |
---
### 3. Notfallrettung & Wartung
* **Integritätsprüfung (FSCK)**:
```powershell
sanctum.exe verify --path "D:\Tresor\daten.sanctum"
```
Überprüft die B-Tree-Struktur der Datenbank und testet sämtliche Chunks gegen ihre kryptografischen AEAD-Authentifizierungs-Tags.
* **Online-Backup im laufenden Betrieb**:
```powershell
sanctum.exe backup --path "D:\Tresor\daten.sanctum" --output "E:\Backup\daten_backup.sanctum"
```
Erzeugt über die SQLite Online Backup API eine konsistente Kopie selbst während Dateien geöffnet sind.
* **Passwort vergessen? Wiederherstellung via BIP-39 Notfallschlüssel**:
```powershell
sanctum.exe passwd --path "D:\Tresor\daten.sanctum" --recovery-key
```
*(Liest die 24 Wörter maskiert ein, ohne Spuren in der PowerShell-Historie zu hinterlassen, und vergibt ein neues Passwort).*
---
## 🖨️ Druckvorlage: BIP-39 Notfallkarte
Drucke diesen Abschnitt aus oder übertrage die Wörter handschriftlich auf ein Blatt Papier. Bewahre diese Karte physisch getrennt von deinem Computer an einem sicheren Ort (z. B. Tresor, Dokumentenmappe) auf.
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ SANCTUM — KRYPTOGRAFISCHE NOTFALL-WIEDERHERSTELLUNGSKARTE (BIP-39) │
└──────────────────────────────────────────────────────────────────────────────┘
Container: __________________________________________________________________
Erstelldatum: ____.___.202__ Safe: [ ] Standard [ ] Hidden Vault
Hinweis: Alle Wörter sind in Kleinbuchstaben aus der offiziellen BIP-39 Liste.
┌────┬────────────────────────────┬────┬────────────────────────────┐
│ # │ WORT │ # │ WORT │
├────┼────────────────────────────┼────┼────────────────────────────┤
│ 01 │ __________________________ │ 13 │ __________________________ │
│ 02 │ __________________________ │ 14 │ __________________________ │
│ 03 │ __________________________ │ 15 │ __________________________ │
│ 04 │ __________________________ │ 16 │ __________________________ │
│ 05 │ __________________________ │ 17 │ __________________________ │
│ 06 │ __________________________ │ 18 │ __________________________ │
│ 07 │ __________________________ │ 19 │ __________________________ │
│ 08 │ __________________________ │ 20 │ __________________________ │
│ 09 │ __________________________ │ 21 │ __________________________ │
│ 10 │ __________________________ │ 22 │ __________________________ │
│ 11 │ __________________________ │ 23 │ __________________________ │
│ 12 │ __________________________ │ 24 │ __________________________ │
└────┴────────────────────────────┴────┴────────────────────────────┘
⚠️ SICHERHEITSHINWEISE:
1. Wer im Besitz dieser 24 Wörter ist, kann den Tresor ohne Passwort entschlüsseln!
2. Niemals abfotografieren, in Cloud-Notizen speichern oder unverschlüsselt versenden.
3. Bei Verlust beider Passwörter und dieser Karte sind die Daten unwiederbringlich verloren.
```
+4 -1
View File
@@ -17,6 +17,8 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
- **Disaster Recovery**: 24-Wort BIP-39 Mnemonic Seed Phrases, konsistente Online-Backups via SQLite Online Backup API und kryptografische Vollprüfung (`sanctum verify`). - **Disaster Recovery**: 24-Wort BIP-39 Mnemonic Seed Phrases, konsistente Online-Backups via SQLite Online Backup API und kryptografische Vollprüfung (`sanctum verify`).
- **Statisches Single-Binary**: `sanctum.exe` (~5.3 MB) ohne externe DLL-Abhängigkeiten. - **Statisches Single-Binary**: `sanctum.exe` (~5.3 MB) ohne externe DLL-Abhängigkeiten.
> 💡 **Neu bei Sanctum?** Eine kompakte Schritt-für-Schritt-Anleitung findest du in der [Schnellstartanleitung (QUICKSTART.md)](QUICKSTART.md). Hinweise zur Installation via Scoop oder Winget gibt es im [Installations-Guide (INSTALL.md)](INSTALL.md).
--- ---
## 🔐 Kryptografie & Sicherheitsarchitektur ## 🔐 Kryptografie & Sicherheitsarchitektur
@@ -209,7 +211,8 @@ powershell -ExecutionPolicy Bypass -File .\scripts\package-release.ps1
``` ```
Erzeugt: Erzeugt:
- `dist/sanctum-v0.3.1-windows-x86_64.zip` - `dist/sanctum-v0.4.0-windows-x86_64.zip` (Windows x86_64 ZIP)
- `dist/sanctum-v0.4.0-linux-x86_64.tar.gz` (Linux x86_64 musl static TAR.GZ via `package-release-linux.ps1`)
- `dist/SHA256SUMS.txt` - `dist/SHA256SUMS.txt`
--- ---
+25
View File
@@ -0,0 +1,25 @@
{
"version": "0.4.0",
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
"homepage": "https://gitea.pansi.eu/harald/sanctum",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.4.0/sanctum-v0.4.0-windows-x86_64.zip",
"hash": "0602173dceeb082fdbe7f64b73a37de1e3f7b021390386a9efba8f43aa65e76d",
"bin": "sanctum.exe"
}
},
"checkver": {
"url": "https://gitea.pansi.eu/api/v1/repos/harald/sanctum/releases/latest",
"jsonpath": "$.tag_name",
"regex": "v([\\d.]+)"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v$version/sanctum-v$version-windows-x86_64.zip"
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
PackageIdentifier: HaraldPansi.Sanctum
PackageVersion: 0.4.0
PackageName: Sanctum
Publisher: Harald Pansi
PublisherUrl: https://gitea.pansi.eu/harald
PublisherSupportUrl: https://gitea.pansi.eu/harald/sanctum/issues
Author: Harald Pansi
License: MIT
LicenseUrl: https://gitea.pansi.eu/harald/sanctum/src/branch/main/LICENSE
ShortDescription: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV
Description: Sanctum ist eine eigenständige, speichersichere CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (.sanctum) unter Windows 10/11 im reinen Userland verwaltet. 100% Userland, keine Adminrechte, keine Treiber.
Moniker: sanctum
Tags:
- encryption
- security
- privacy
- webdav
- container
- plausible-deniability
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.4.0
Installers:
- Architecture: x64
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: sanctum.exe
PortableCommandAlias: sanctum
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.4.0/sanctum-v0.4.0-windows-x86_64.zip
InstallerSha256: 0602173dceeb082fdbe7f64b73a37de1e3f7b021390386a9efba8f43aa65e76d
ManifestType: singleton
ManifestVersion: 1.6.0
+119
View File
@@ -0,0 +1,119 @@
param (
[switch]$SkipBuild = $false
)
$ErrorActionPreference = "Stop"
# Repository-Wurzelverzeichnis ermitteln
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectRoot = Split-Path -Parent $ScriptDir
Set-Location $ProjectRoot
# Toolchain-Pfade sicherstellen (Zig + Cargo Bin)
$ZigDir = "C:\Users\pansih\AppData\Local\Microsoft\WinGet\Packages\zig.zig_Microsoft.Winget.Source_8wekyb3d8bbwe\zig-x86_64-windows-0.16.0"
$env:PATH = "$ZigDir;C:\Users\pansih\.cargo\bin;C:\Windows\System32;" + $env:PATH
# Version aus Cargo.toml auslesen
$CargoToml = Get-Content (Join-Path $ProjectRoot "Cargo.toml") -Raw
if ($CargoToml -match 'version\s*=\s*"([^"]+)"') {
$Version = $matches[1]
} else {
Write-Error "Konnte Versionsnummer nicht aus Cargo.toml ermitteln."
exit 1
}
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Sanctum Linux Release Packaging v$Version (x86_64 musl)" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
# 1. Linux Release-Binary bauen
$Target = "x86_64-unknown-linux-musl"
$BinaryPath = Join-Path $ProjectRoot "target\$Target\release\sanctum"
if (-not $SkipBuild -or -not (Test-Path $BinaryPath)) {
Write-Host "`n[1/3] Kompiliere statisches Linux-Binary via cargo-zigbuild..." -ForegroundColor Yellow
cargo-zigbuild.exe zigbuild --target $Target --release
if ($LASTEXITCODE -ne 0) {
Write-Error "Linux-Build fehlgeschlagen!"
exit $LASTEXITCODE
}
Write-Host "[OK] Linux-Binary erfolgreich gebaut." -ForegroundColor Green
} else {
Write-Host "`n[1/3] Build uebersprungen (-SkipBuild)." -ForegroundColor DarkYellow
}
# 2. Release-Verzeichnisstruktur vorbereiten
Write-Host "`n[2/3] Erstelle Linux-Distributionspaket..." -ForegroundColor Yellow
$DistDir = Join-Path $ProjectRoot "dist"
if (-not (Test-Path $DistDir)) {
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
}
$PackageName = "sanctum-v$Version-linux-x86_64"
$StagingDir = Join-Path $DistDir $PackageName
$TarGzFile = Join-Path $DistDir "$PackageName.tar.gz"
if (Test-Path $StagingDir) {
Remove-Item $StagingDir -Recurse -Force
}
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
# Dateien kopieren
Copy-Item $BinaryPath -Destination (Join-Path $StagingDir "sanctum") -Force
Copy-Item (Join-Path $ProjectRoot "README.md") -Destination $StagingDir -Force
Copy-Item (Join-Path $ProjectRoot "LICENSE") -Destination $StagingDir -Force
Copy-Item (Join-Path $ProjectRoot "CHANGELOG.md") -Destination $StagingDir -Force
Copy-Item (Join-Path $ProjectRoot "INSTALL.md") -Destination $StagingDir -Force
Copy-Item (Join-Path $ProjectRoot "QUICKSTART.md") -Destination $StagingDir -Force
Copy-Item (Join-Path $ProjectRoot "LEGAL.md") -Destination $StagingDir -Force
Copy-Item (Join-Path $ProjectRoot "THIRD_PARTY_LICENSES.md") -Destination $StagingDir -Force
# .tar.gz Archiv erstellen mit Windows nativem bsdtar
if (Test-Path $TarGzFile) {
Remove-Item $TarGzFile -Force
}
tar.exe -czf $TarGzFile -C $DistDir $PackageName
if ($LASTEXITCODE -ne 0) {
Write-Error "Fehler beim Erstellen des tar.gz-Archivs!"
exit $LASTEXITCODE
}
# Staging-Verzeichnis bereinigen
Remove-Item $StagingDir -Recurse -Force
Write-Host "[OK] Archiv erstellt: $TarGzFile" -ForegroundColor Green
# 3. SHA-256 Pruefsummen berechnen und aktualisieren
Write-Host "`n[3/3] Aktualisiere SHA-256 Pruefsummen..." -ForegroundColor Yellow
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
$TarHash = (Get-FileHash -Path $TarGzFile -Algorithm SHA256).Hash.ToLower()
$ElfHash = (Get-FileHash -Path $BinaryPath -Algorithm SHA256).Hash.ToLower()
# Existierende Pruefsummen lesen und um Linux-Assets ergaenzen/aktualisieren
$ExistingLines = if (Test-Path $ChecksumFile) {
Get-Content $ChecksumFile | Where-Object { $_ -notmatch "$PackageName\.tar\.gz" -and $_ -notmatch "\s+sanctum$" }
} else {
@()
}
$AllLines = @()
$AllLines += $ExistingLines
$AllLines += "$TarHash $PackageName.tar.gz"
$AllLines += "$ElfHash sanctum"
$AllLines | Set-Content -Path $ChecksumFile -Encoding utf8
$TarSizeMB = [math]::Round((Get-Item $TarGzFile).Length / 1MB, 2)
$ElfSizeMB = [math]::Round((Get-Item $BinaryPath).Length / 1MB, 2)
Write-Host "`n============================================================" -ForegroundColor Green
Write-Host " Sanctum Linux Release v$Version erfolgreich gepackt!" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host " Archiv: $TarGzFile ($TarSizeMB MB)"
Write-Host " TAR SHA-256: $TarHash"
Write-Host " ELF SHA-256: $ElfHash"
Write-Host " Checksum-File: $ChecksumFile"
Write-Host "`nBereit fuer Gitea Release."
+18 -3
View File
@@ -55,14 +55,19 @@ $PackageName = "sanctum-v$Version-windows-x86_64"
$StagingDir = Join-Path $DistDir $PackageName $StagingDir = Join-Path $DistDir $PackageName
$ZipFile = Join-Path $DistDir "$PackageName.zip" $ZipFile = Join-Path $DistDir "$PackageName.zip"
if (Test-Path $DistDir) { if (-not (Test-Path $DistDir)) {
Remove-Item $DistDir -Recurse -Force New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
}
if (Test-Path $StagingDir) {
Remove-Item $StagingDir -Recurse -Force
} }
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
$ExeSource = Join-Path $ProjectRoot "target\release\sanctum.exe" $ExeSource = Join-Path $ProjectRoot "target\release\sanctum.exe"
Copy-Item $ExeSource (Join-Path $StagingDir "sanctum.exe") Copy-Item $ExeSource (Join-Path $StagingDir "sanctum.exe")
Copy-Item (Join-Path $ProjectRoot "README.md") (Join-Path $StagingDir "README.md") Copy-Item (Join-Path $ProjectRoot "README.md") (Join-Path $StagingDir "README.md")
Copy-Item (Join-Path $ProjectRoot "QUICKSTART.md") (Join-Path $StagingDir "QUICKSTART.md")
Copy-Item (Join-Path $ProjectRoot "INSTALL.md") (Join-Path $StagingDir "INSTALL.md")
Copy-Item (Join-Path $ProjectRoot "LICENSE") (Join-Path $StagingDir "LICENSE") Copy-Item (Join-Path $ProjectRoot "LICENSE") (Join-Path $StagingDir "LICENSE")
Copy-Item (Join-Path $ProjectRoot "LEGAL.md") (Join-Path $StagingDir "LEGAL.md") Copy-Item (Join-Path $ProjectRoot "LEGAL.md") (Join-Path $StagingDir "LEGAL.md")
Copy-Item (Join-Path $ProjectRoot "THIRD_PARTY_LICENSES.md") (Join-Path $StagingDir "THIRD_PARTY_LICENSES.md") Copy-Item (Join-Path $ProjectRoot "THIRD_PARTY_LICENSES.md") (Join-Path $StagingDir "THIRD_PARTY_LICENSES.md")
@@ -82,7 +87,17 @@ $ZipHash = (Get-FileHash -Path $ZipFile -Algorithm SHA256).Hash.ToLower()
$ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower() $ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower()
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt" $ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
@("$ZipHash $PackageName.zip", "$ExeHash sanctum.exe") | Set-Content -Path $ChecksumFile -Encoding utf8 $ExistingLines = if (Test-Path $ChecksumFile) {
Get-Content $ChecksumFile | Where-Object { $_ -notmatch "$PackageName\.zip" -and $_ -notmatch "\s+sanctum\.exe$" }
} else {
@()
}
$AllLines = @()
$AllLines += "$ZipHash $PackageName.zip"
$AllLines += "$ExeHash sanctum.exe"
$AllLines += $ExistingLines
$AllLines | Set-Content -Path $ChecksumFile -Encoding utf8
Write-Host "[OK] Pruefsummen in SHA256SUMS.txt gespeichert." -ForegroundColor Green Write-Host "[OK] Pruefsummen in SHA256SUMS.txt gespeichert." -ForegroundColor Green
+47 -21
View File
@@ -108,29 +108,55 @@ if (-not $Release) {
$UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets" $UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets"
$AuthHeader = "Authorization: token $Token" $AuthHeader = "Authorization: token $Token"
# ZIP Asset hochladen # Hilfsfunktion zum sauberen Hochladen/Ersetzen von Assets
Write-Host "Lade sanctum-$TagName-windows-x86_64.zip hoch..." -ForegroundColor Cyan function Upload-ReleaseAsset {
$ZipPathNorm = $ZipFile.Replace('\', '/') param (
$ZipUrl = "$UploadUrl?name=sanctum-$TagName-windows-x86_64.zip" [string]$FilePath,
$ZipResult = & curl.exe -s -X POST $ZipUrl -H $AuthHeader -F "attachment=@$ZipPathNorm" [string]$AssetName
Write-Host "[OK] sanctum-$TagName-windows-x86_64.zip hochgeladen." -ForegroundColor Green )
if (-not (Test-Path $FilePath)) {
return
}
Write-Host "Lade $AssetName hoch..." -ForegroundColor Cyan
# Vorhandenes Asset mit gleichem Namen ermitteln und ggf. loeschen
$ExistingAssets = try {
Invoke-RestMethod -Uri $UploadUrl -Headers $Headers -Method Get
} catch {
@()
}
$Duplicate = $ExistingAssets | Where-Object { $_.name -eq $AssetName }
if ($Duplicate) {
Write-Host " [i] Ersetze existierendes Asset '$AssetName' (ID: $($Duplicate.id))..." -ForegroundColor DarkYellow
$DeleteUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets/$($Duplicate.id)"
Invoke-RestMethod -Uri $DeleteUrl -Headers $Headers -Method Delete | Out-Null
}
$PathNorm = $FilePath.Replace('\', '/')
$TargetUrl = "${UploadUrl}?name=$AssetName"
$Result = & curl.exe -sS -X POST $TargetUrl -H $AuthHeader -F "attachment=@$PathNorm"
if ($LASTEXITCODE -ne 0) {
Write-Error "Upload von $AssetName fehlgeschlagen: $Result"
exit 1
}
Write-Host "[OK] $AssetName erfolgreich hochgeladen." -ForegroundColor Green
}
# Windows-Assets hochladen
Upload-ReleaseAsset -FilePath $ZipFile -AssetName "sanctum-$TagName-windows-x86_64.zip"
$ExeFile = Join-Path $ProjectRoot "target\release\sanctum.exe"
Upload-ReleaseAsset -FilePath $ExeFile -AssetName "sanctum.exe"
# Linux-Assets hochladen
$LinuxTar = Join-Path $DistDir "sanctum-$TagName-linux-x86_64.tar.gz"
Upload-ReleaseAsset -FilePath $LinuxTar -AssetName "sanctum-$TagName-linux-x86_64.tar.gz"
$LinuxElf = Join-Path $ProjectRoot "target\x86_64-unknown-linux-musl\release\sanctum"
Upload-ReleaseAsset -FilePath $LinuxElf -AssetName "sanctum"
# SHA256SUMS.txt hochladen # SHA256SUMS.txt hochladen
Write-Host "Lade SHA256SUMS.txt hoch..." -ForegroundColor Cyan Upload-ReleaseAsset -FilePath $ChecksumFile -AssetName "SHA256SUMS.txt"
$SumPathNorm = $ChecksumFile.Replace('\', '/')
$SumUrl = "$UploadUrl?name=SHA256SUMS.txt"
$SumResult = & curl.exe -s -X POST $SumUrl -H $AuthHeader -F "attachment=@$SumPathNorm"
Write-Host "[OK] SHA256SUMS.txt hochgeladen." -ForegroundColor Green
# Standalone EXE hochladen
$ExeFile = Join-Path $ProjectRoot "target\release\sanctum.exe"
if (Test-Path $ExeFile) {
Write-Host "Lade sanctum.exe hoch..." -ForegroundColor Cyan
$ExePathNorm = $ExeFile.Replace('\', '/')
$ExeUrl = "$UploadUrl?name=sanctum.exe"
$ExeResult = & curl.exe -s -X POST $ExeUrl -H $AuthHeader -F "attachment=@$ExePathNorm"
Write-Host "[OK] sanctum.exe hochgeladen." -ForegroundColor Green
}
Write-Host "`n============================================================" -ForegroundColor Green Write-Host "`n============================================================" -ForegroundColor Green
+1
View File
@@ -1,6 +1,7 @@
pub mod carrier; pub mod carrier;
pub mod crypto; pub mod crypto;
pub mod mount; pub mod mount;
pub mod platform;
pub mod recovery; pub mod recovery;
pub mod storage; pub mod storage;
pub mod ui; pub mod ui;
+22 -9
View File
@@ -68,10 +68,14 @@ enum Commands {
#[arg(short, long)] #[arg(short, long)]
path: PathBuf, path: PathBuf,
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt standardmäßig automatisch das nächste freie Laufwerk) /// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt unter Windows standardmäßig das nächste freie Laufwerk)
#[arg(short, long)] #[arg(short, long)]
drive: Option<String>, drive: Option<String>,
/// Optionaler lokaler Mount-Pfad unter Linux/macOS (z. B. '/mnt/sanctum' oder '~/vault')
#[arg(short = 'm', long = "mount-point", value_name = "DIR")]
mount_point: Option<PathBuf>,
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443) /// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
#[arg(long)] #[arg(long)]
port: Option<u16>, port: Option<u16>,
@@ -904,6 +908,7 @@ async fn run() -> Result<()> {
Commands::Mount { Commands::Mount {
path, path,
drive, drive,
mount_point,
port, port,
recovery_key, recovery_key,
no_open, no_open,
@@ -916,15 +921,22 @@ async fn run() -> Result<()> {
let drive_char = match drive { let drive_char = match drive {
Some(ref d) => parse_drive_letter(d)?, Some(ref d) => parse_drive_letter(d)?,
None => { None => {
let auto_drive = sanctum::windows::find_next_available_drive()?; #[cfg(windows)]
if !stealth { {
println!( let auto_drive = sanctum::windows::find_next_available_drive()?;
" {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:", if !stealth {
ui::cyan(""), println!(
auto_drive " {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:",
); ui::cyan(""),
auto_drive
);
}
auto_drive
}
#[cfg(not(windows))]
{
'Z'
} }
auto_drive
} }
}; };
let auth = if let Some(key) = recovery_key { let auth = if let Some(key) = recovery_key {
@@ -953,6 +965,7 @@ async fn run() -> Result<()> {
mount_container( mount_container(
&path, &path,
drive_char, drive_char,
mount_point.as_deref(),
port, port,
auth, auth,
open_explorer, open_explorer,
+242 -87
View File
@@ -32,87 +32,119 @@ pub fn format_drive(drive_letter: char) -> String {
format!("{}:", drive_letter.to_ascii_uppercase()) format!("{}:", drive_letter.to_ascii_uppercase())
} }
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`. /// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y` bzw. Unix-Mountpoint via `gio mount -u`.
pub fn unmount_drive(drive_letter: char) -> Result<()> { pub fn unmount_drive(drive_letter: char) -> Result<()> {
let drive_str = format_drive(drive_letter); #[cfg(windows)]
{
let drive_str = format_drive(drive_letter);
let output = Command::new("net") let output = Command::new("net")
.args(["use", &drive_str, "/delete", "/y"]) .args(["use", &drive_str, "/delete", "/y"])
.output() .output()
.context("Fehler beim Ausführen des Befehls 'net use'")?; .context("Fehler beim Ausführen des Befehls 'net use'")?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
bail!( bail!(
"Netzlaufwerk {} konnte nicht getrennt werden:\n{}{}", "Netzlaufwerk {} konnte nicht getrennt werden:\n{}{}",
drive_str, drive_str,
stdout, stdout,
stderr stderr
); );
}
Ok(())
}
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /persistent:no` ein.
fn run_net_use_mount(drive_str: &str, port: u16, session_token: &str) -> Result<()> {
let url = format!("http://127.0.0.1:{}/{}/", port, session_token);
let mut output = Command::new("net")
.args(["use", drive_str, &url, "/persistent:no"])
.output()
.context("Fehler beim Ausführen des Befehls 'net use'")?;
// PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85)
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let is_already_in_use = stderr.contains("85")
|| stderr.contains("bereits verwendet")
|| stderr.contains("already in use")
|| stdout.contains("85")
|| stdout.contains("bereits verwendet")
|| stdout.contains("already in use");
if is_already_in_use {
debug!("Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...", drive_str);
let _ = Command::new("net")
.args(["use", drive_str, "/delete", "/y"])
.output();
// Zweiter Versuch nach automatischer Bereinigung
output = Command::new("net")
.args(["use", drive_str, &url, "/persistent:no"])
.output()
.context("Fehler beim erneuten Ausführen des Befehls 'net use'")?;
} }
}
if !output.status.success() { Ok(())
let stderr = String::from_utf8_lossy(&output.stderr); }
let stdout = String::from_utf8_lossy(&output.stdout); #[cfg(unix)]
let webclient_hint = if stderr.contains("67") || stderr.contains("Netzwerkname") || stderr.contains("Systemfehler") { {
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'." let _ = drive_letter;
} else { Ok(())
"" }
}; #[cfg(not(any(windows, unix)))]
bail!( {
"Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}", let _ = drive_letter;
drive_str, Ok(())
stdout,
stderr,
webclient_hint
);
} }
Ok(())
} }
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk ein. /// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /persistent:no` ein
/// bzw. unter Unix via `gio mount dav://127.0.0.1:<PORT>/<TOKEN>/`.
fn run_mount_command(drive_str: &str, port: u16, session_token: &str) -> Result<()> {
#[cfg(windows)]
{
let url = format!("http://127.0.0.1:{}/{}/", port, session_token);
let mut output = Command::new("net")
.args(["use", drive_str, &url, "/persistent:no"])
.output()
.context("Fehler beim Ausführen des Befehls 'net use'")?;
// PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85)
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let is_already_in_use = stderr.contains("85")
|| stderr.contains("bereits verwendet")
|| stderr.contains("already in use")
|| stdout.contains("85")
|| stdout.contains("bereits verwendet")
|| stdout.contains("already in use");
if is_already_in_use {
debug!("Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...", drive_str);
let _ = Command::new("net")
.args(["use", drive_str, "/delete", "/y"])
.output();
// Zweiter Versuch nach automatischer Bereinigung
output = Command::new("net")
.args(["use", drive_str, &url, "/persistent:no"])
.output()
.context("Fehler beim erneuten Ausführen des Befehls 'net use'")?;
}
}
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let webclient_hint = if stderr.contains("67") || stderr.contains("Netzwerkname") || stderr.contains("Systemfehler") {
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'."
} else {
""
};
bail!(
"Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}",
drive_str,
stdout,
stderr,
webclient_hint
);
}
Ok(())
}
#[cfg(unix)]
{
let dav_url = format!("dav://127.0.0.1:{}/{}/", port, session_token);
let _ = Command::new("gio")
.args(["mount", &dav_url])
.output();
let _ = drive_str;
Ok(())
}
#[cfg(not(any(windows, unix)))]
{
let _ = (drive_str, port, session_token);
Ok(())
}
}
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk (Windows) bzw. VFS (Linux) ein.
pub async fn mount_container( pub async fn mount_container(
container_path: &Path, container_path: &Path,
drive_letter: char, drive_letter: char,
mount_point: Option<&Path>,
requested_port: Option<u16>, requested_port: Option<u16>,
auth: ContainerAuth, auth: ContainerAuth,
open_explorer: bool, open_explorer: bool,
@@ -123,6 +155,10 @@ pub async fn mount_container(
stealth: bool, stealth: bool,
) -> Result<()> { ) -> Result<()> {
let drive_str = format_drive(drive_letter); let drive_str = format_drive(drive_letter);
let _ = mount_point;
let _ = open_explorer;
let _ = enable_tray;
let _ = lock_on_screen_lock;
if !container_path.exists() { if !container_path.exists() {
bail!( bail!(
@@ -224,6 +260,7 @@ pub async fn mount_container(
let bound_port = bound_addr.port(); let bound_port = bound_addr.port();
if !stealth { if !stealth {
#[cfg(windows)]
ui::step( ui::step(
4, 4,
4, 4,
@@ -233,6 +270,16 @@ pub async fn mount_container(
bound_port, drive_str bound_port, drive_str
), ),
); );
#[cfg(not(windows))]
ui::step(
4,
4,
"🌐",
&format!(
"Starte WebDAV-Server auf Port {} & initialisiere VFS...",
bound_port
),
);
} }
let (shutdown_tx, shutdown_rx) = watch::channel(false); let (shutdown_tx, shutdown_rx) = watch::channel(false);
@@ -245,20 +292,22 @@ pub async fn mount_container(
shutdown_rx, shutdown_rx,
)); ));
// Netzlaufwerk einbinden // Netzlaufwerk bzw. Verzeichnis einbinden
if let Err(e) = run_net_use_mount(&drive_str, bound_port, &session_token) { if let Err(e) = run_mount_command(&drive_str, bound_port, &session_token) {
let _ = shutdown_tx.send(true); let _ = shutdown_tx.send(true);
let _ = server_handle.await; let _ = server_handle.await;
return Err(e); return Err(e);
} }
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault) // Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
#[cfg(windows)]
if open_explorer { if open_explorer {
let _ = crate::windows::open_in_explorer(drive_letter); let _ = crate::windows::open_in_explorer(drive_letter);
} }
// System-Tray Initialisierung // System-Tray Initialisierung (nur Windows)
let (tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); #[cfg(windows)]
let (_tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
#[cfg(windows)] #[cfg(windows)]
let _tray = if enable_tray { let _tray = if enable_tray {
let icon_source = crate::windows::get_default_system_icon() let icon_source = crate::windows::get_default_system_icon()
@@ -276,7 +325,7 @@ pub async fn mount_container(
let _ = tray.add_menu_item("Im Explorer öffnen", move || { let _ = tray.add_menu_item("Im Explorer öffnen", move || {
let _ = crate::windows::open_in_explorer(dl); let _ = crate::windows::open_in_explorer(dl);
}); });
let s_tx = tray_shutdown_tx.clone(); let s_tx = _tray_shutdown_tx.clone();
let _ = tray.add_menu_item("Trennen & Beenden", move || { let _ = tray.add_menu_item("Trennen & Beenden", move || {
let _ = s_tx.blocking_send(()); let _ = s_tx.blocking_send(());
}); });
@@ -315,8 +364,10 @@ pub async fn mount_container(
} }
} }
// Windows-Sitzungssperre (Win + L Auto-Lock) // Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
#[cfg(windows)]
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1); let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
#[cfg(windows)]
let _session_monitor = if lock_on_screen_lock { let _session_monitor = if lock_on_screen_lock {
match crate::windows::start_session_lock_monitor(session_lock_tx) { match crate::windows::start_session_lock_monitor(session_lock_tx) {
Ok(guard) => Some(guard), Ok(guard) => Some(guard),
@@ -330,7 +381,10 @@ pub async fn mount_container(
}; };
if stealth { if stealth {
#[cfg(windows)]
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str); println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
#[cfg(not(windows))]
println!("Sanctum: WebDAV-Server auf Port {} bereit.", bound_port);
} else { } else {
println!(); println!();
println!("┌─────────────────────────────────────────────────────────────┐"); println!("┌─────────────────────────────────────────────────────────────┐");
@@ -338,30 +392,63 @@ pub async fn mount_container(
println!("└─────────────────────────────────────────────────────────────┘"); println!("└─────────────────────────────────────────────────────────────┘");
println!(); println!();
println!(" • Container: {}", container_path.display()); println!(" • Container: {}", container_path.display());
#[cfg(windows)]
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str)); println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
#[cfg(not(windows))]
{
if let Some(mp) = mount_point {
println!(" • Mountpoint: {}", ui::cyan(&mp.display().to_string()));
} else {
println!(" • Modus: WebDAV Userland-VFS");
}
}
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token); println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
#[cfg(not(windows))]
{
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/{}/", bound_port, session_token);
if let Some(mp) = mount_point {
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/{}/ {}", bound_port, session_token, mp.display());
}
}
if let Some(secs) = idle_timeout { if let Some(secs) = idle_timeout {
println!(" • Auto-Lock: Inaktivität nach {}s", secs); println!(" • Auto-Lock: Inaktivität nach {}s", secs);
} }
#[cfg(windows)]
if lock_on_screen_lock { if lock_on_screen_lock {
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv"); println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
} }
if anti_leak { if anti_leak {
#[cfg(windows)]
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)"); println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
#[cfg(not(windows))]
println!(" • Anti-Leak: VFS-Metadatenfilter aktiv (.directory, .Trash, desktop.ini blockiert)");
} }
#[cfg(windows)]
if enable_tray { if enable_tray {
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)"); println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
} }
println!(); println!();
#[cfg(windows)]
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp")); println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
#[cfg(not(windows))]
println!(" [{}] Drücke [Ctrl+C] zum sicheren Beenden.", ui::yellow("Tipp"));
println!(); println!();
} }
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT) // Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT, nur Windows)
#[cfg(windows)]
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1); let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
#[cfg(windows)]
let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok(); let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok();
// Unix Signale (SIGTERM, SIGHUP)
#[cfg(unix)]
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
#[cfg(unix)]
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen) // Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
#[cfg(windows)]
tokio::select! { tokio::select! {
res = tokio::signal::ctrl_c() => { res = tokio::signal::ctrl_c() => {
let _ = res; let _ = res;
@@ -370,25 +457,81 @@ pub async fn mount_container(
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
} }
} }
_ = console_close_rx.recv() => { Some(()) = console_close_rx.recv() => {
if !stealth { if !stealth {
println!(); println!();
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]")); println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
} }
} }
_ = tray_shutdown_rx.recv() => { Some(()) = tray_shutdown_rx.recv() => {
if !stealth { if !stealth {
println!(); println!();
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]")); println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
} }
} }
_ = session_lock_rx.recv() => { Some(()) = session_lock_rx.recv() => {
if !stealth { if !stealth {
println!(); println!();
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]")); println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
} }
} }
_ = idle_shutdown_rx.recv() => { Some(()) = idle_shutdown_rx.recv() => {
if !stealth {
println!();
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
}
}
}
#[cfg(unix)]
tokio::select! {
res = tokio::signal::ctrl_c() => {
let _ = res;
if !stealth {
println!();
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
}
}
_ = async {
match sigterm.as_mut() {
Some(s) => { s.recv().await; }
None => { std::future::pending::<()>().await; }
}
} => {
if !stealth {
println!();
println!(" {} Beendigungssignal (SIGTERM) empfangen.", ui::yellow("[!]"));
}
}
_ = async {
match sighup.as_mut() {
Some(s) => { s.recv().await; }
None => { std::future::pending::<()>().await; }
}
} => {
if !stealth {
println!();
println!(" {} Beendigungssignal (SIGHUP) empfangen.", ui::yellow("[!]"));
}
}
Some(()) = idle_shutdown_rx.recv() => {
if !stealth {
println!();
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
}
}
}
#[cfg(not(any(windows, unix)))]
tokio::select! {
res = tokio::signal::ctrl_c() => {
let _ = res;
if !stealth {
println!();
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
}
}
Some(()) = idle_shutdown_rx.recv() => {
if !stealth { if !stealth {
println!(); println!();
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]")); println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
@@ -406,15 +549,27 @@ pub async fn mount_container(
} }
} }
let _ = db.checkpoint(); let _ = db.checkpoint();
#[cfg(windows)]
println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str); println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str);
#[cfg(not(windows))]
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
} else { } else {
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str); #[cfg(windows)]
let _ = std::io::Write::flush(&mut std::io::stdout()); {
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
let _ = std::io::Write::flush(&mut std::io::stdout());
// Automatisches Unmount // Automatisches Unmount
if let Err(e) = unmount_drive(drive_letter) { if let Err(e) = unmount_drive(drive_letter) {
println!("{}", ui::yellow(&format!("Warnung ({e})"))); println!("{}", ui::yellow(&format!("Warnung ({e})")));
} else { } else {
println!("{}", ui::green("OK"));
}
}
#[cfg(not(windows))]
{
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]"));
let _ = std::io::Write::flush(&mut std::io::stdout());
println!("{}", ui::green("OK")); println!("{}", ui::green("OK"));
} }
+7
View File
@@ -0,0 +1,7 @@
//! Plattform-Abstraktionsschicht für Sanctum.
//!
//! Dieses Modul bündelt alle betriebssystemspezifischen Funktionen
//! (Speichersperren, Dateimanager-Aufrufe, Signal-Monitoring, Shell-Integration)
//! für Windows, Linux und macOS unter einer einheitlichen, speichersicheren Schnittstelle.
pub use crate::windows::*;
+212 -96
View File
@@ -1,5 +1,6 @@
use std::process::Command; use std::process::Command;
#[allow(unused_imports)]
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D'). /// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
@@ -32,14 +33,68 @@ pub fn find_next_available_drive() -> Result<char> {
} }
} }
/// Öffnet das eingebundene Netzlaufwerk direkt im Windows Explorer. /// Öffnet das eingebundene Netzlaufwerk oder Verzeichnis direkt im systemeigenen Dateimanager
/// (Windows: Explorer, macOS: open, Linux: xdg-open).
pub fn open_in_explorer(drive_char: char) -> Result<()> { pub fn open_in_explorer(drive_char: char) -> Result<()> {
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase()); #[cfg(windows)]
Command::new("explorer.exe") {
.arg(&drive_path) let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
.spawn() Command::new("explorer.exe")
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?; .arg(&drive_path)
Ok(()) .spawn()
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
Ok(())
}
#[cfg(target_os = "macos")]
{
let _ = drive_char;
let _ = Command::new("open").arg(".").spawn();
Ok(())
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let _ = drive_char;
let _ = Command::new("xdg-open").arg(".").spawn();
Ok(())
}
#[cfg(not(any(windows, unix)))]
{
let _ = drive_char;
Ok(())
}
}
/// Öffnet einen beliebigen Pfad im nativen Dateimanager der Plattform.
pub fn open_in_file_manager(path: &std::path::Path) -> Result<()> {
#[cfg(windows)]
{
Command::new("explorer.exe")
.arg(path)
.spawn()
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", path.display()))?;
Ok(())
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.arg(path)
.spawn()
.with_context(|| format!("Konnte macOS Finder für '{}' nicht öffnen", path.display()))?;
Ok(())
}
#[cfg(all(unix, not(target_os = "macos")))]
{
Command::new("xdg-open")
.arg(path)
.spawn()
.with_context(|| format!("Konnte Dateimanager via xdg-open für '{}' nicht öffnen", path.display()))?;
Ok(())
}
#[cfg(not(any(windows, unix)))]
{
let _ = path;
Ok(())
}
} }
/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED). /// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED).
@@ -67,104 +122,146 @@ pub fn notify_shell_associations_changed() {
); );
} }
} }
#[cfg(all(unix, not(target_os = "macos")))]
{
let _ = Command::new("update-desktop-database").spawn();
}
#[cfg(not(any(windows, all(unix, not(target_os = "macos")))))]
{
}
} }
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte). /// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer bzw. unter Linux via Freedesktop.
pub fn register_explorer_integration() -> Result<()> { pub fn register_explorer_integration() -> Result<()> {
let current_exe = std::env::current_exe() #[cfg(windows)]
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?; {
let exe_str = current_exe.display().to_string(); let current_exe = std::env::current_exe()
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?;
let exe_str = current_exe.display().to_string();
let reg_commands = [ let reg_commands = [
// 1. .sanctum Erweiterung mit ProgID verknüpfen // 1. .sanctum Erweiterung mit ProgID verknüpfen
( (
r"HKCU\Software\Classes\.sanctum", r"HKCU\Software\Classes\.sanctum",
"", "",
"Sanctum.Container", "Sanctum.Container",
), ),
// 2. ProgID Metadaten & Beschreibung // 2. ProgID Metadaten & Beschreibung
( (
r"HKCU\Software\Classes\Sanctum.Container", r"HKCU\Software\Classes\Sanctum.Container",
"", "",
"Sanctum Verschlüsselter Container", "Sanctum Verschlüsselter Container",
), ),
// 3. Icon // 3. Icon
( (
r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon", r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon",
"", "",
&format!("\"{exe_str}\",0"), &format!("\"{exe_str}\",0"),
), ),
// 4. Standard-Doppelklick-Aktion: Mount // 4. Standard-Doppelklick-Aktion: Mount
( (
r"HKCU\Software\Classes\Sanctum.Container\shell\open", r"HKCU\Software\Classes\Sanctum.Container\shell\open",
"", "",
"Als Laufwerk einbinden", "In Sanctum öffnen",
), ),
( (
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command", r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
"", "",
&format!("\"{exe_str}\" mount --path \"%1\""), &format!("\"{exe_str}\" mount \"%1\""),
), ),
// 5. Kontextmenü-Aktion: Integritätsprüfung // 5. Kontextmenü-Aktion: Verify / FSCK
( (
r"HKCU\Software\Classes\Sanctum.Container\shell\verify", r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
"", "",
"Integrität prüfen (FSCK)", "Integrität prüfen (FSCK)",
), ),
( (
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command", r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
"", "",
&format!("\"{exe_str}\" verify --path \"%1\""), &format!("cmd /k \"\"{exe_str}\" verify \"%1\"\""),
), ),
// 6. Kontextmenü-Aktion: Header sichern // 6. Kontextmenü-Aktion: Header-Backup
( (
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header", r"HKCU\Software\Classes\Sanctum.Container\shell\backup",
"", "",
"Header sichern", "Header sichern (Disaster Recovery)",
), ),
( (
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command", r"HKCU\Software\Classes\Sanctum.Container\shell\backup\command",
"", "",
&format!("\"{exe_str}\" backup-header --path \"%1\""), &format!("cmd /k \"\"{exe_str}\" backup-header \"%1\"\""),
), ),
]; ];
for (key, val_name, val_data) in reg_commands { for (key, val_name, val_data) in reg_commands {
let mut cmd = Command::new("reg"); let mut cmd = Command::new("reg");
cmd.arg("add").arg(key); cmd.arg("add").arg(key);
if val_name.is_empty() { if val_name.is_empty() {
cmd.arg("/ve"); cmd.arg("/ve");
} else { } else {
cmd.arg("/v").arg(val_name); cmd.arg("/v").arg(val_name);
}
cmd.arg("/d").arg(val_data).arg("/f");
let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
}
} }
cmd.arg("/d").arg(val_data).arg("/f");
let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?; notify_shell_associations_changed();
if !output.status.success() { Ok(())
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
}
} }
#[cfg(not(windows))]
{
if let Some(home) = std::env::var_os("HOME") {
let home_path = std::path::PathBuf::from(home);
let apps_dir = home_path.join(".local/share/applications");
let mime_dir = home_path.join(".local/share/mime/packages");
let _ = std::fs::create_dir_all(&apps_dir);
let _ = std::fs::create_dir_all(&mime_dir);
notify_shell_associations_changed(); let desktop_content = "[Desktop Entry]\nType=Application\nName=Sanctum\nComment=Verschlüsselter Ein-Datei-Container\nExec=sanctum mount %f\nIcon=security-high\nTerminal=true\nMimeType=application/x-sanctum;\nCategories=Utility;Security;\n";
Ok(()) let _ = std::fs::write(apps_dir.join("sanctum.desktop"), desktop_content);
let mime_content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n <mime-type type=\"application/x-sanctum\">\n <comment>Sanctum Verschlüsselter Container</comment>\n <glob pattern=\"*.sanctum\"/>\n </mime-type>\n</mime-info>\n";
let _ = std::fs::write(mime_dir.join("application-x-sanctum.xml"), mime_content);
notify_shell_associations_changed();
}
Ok(())
}
} }
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU). /// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU) bzw. unter Linux aus Freedesktop.
pub fn unregister_explorer_integration() -> Result<()> { pub fn unregister_explorer_integration() -> Result<()> {
let keys_to_delete = [ #[cfg(windows)]
r"HKCU\Software\Classes\.sanctum", {
r"HKCU\Software\Classes\Sanctum.Container", let keys_to_delete = [
]; r"HKCU\Software\Classes\.sanctum",
r"HKCU\Software\Classes\Sanctum.Container",
];
for key in keys_to_delete { for key in keys_to_delete {
let _ = Command::new("reg") let _ = Command::new("reg")
.args(["delete", key, "/f"]) .args(["delete", key, "/f"])
.output(); .output();
}
notify_shell_associations_changed();
Ok(())
}
#[cfg(not(windows))]
{
if let Some(home) = std::env::var_os("HOME") {
let home_path = std::path::PathBuf::from(home);
let _ = std::fs::remove_file(home_path.join(".local/share/applications/sanctum.desktop"));
let _ = std::fs::remove_file(home_path.join(".local/share/mime/packages/application-x-sanctum.xml"));
notify_shell_associations_changed();
}
Ok(())
} }
notify_shell_associations_changed();
Ok(())
} }
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray. /// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
@@ -198,10 +295,9 @@ pub fn get_default_system_icon() -> Option<tray_item::IconSource> {
/// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre. /// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre.
/// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet. /// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet.
#[cfg(windows)]
pub struct SessionLockGuard { pub struct SessionLockGuard {
#[cfg(windows)]
hwnd: isize, hwnd: isize,
#[cfg(windows)]
join_handle: Option<std::thread::JoinHandle<()>>, join_handle: Option<std::thread::JoinHandle<()>>,
} }
@@ -527,7 +623,7 @@ pub fn start_console_ctrl_monitor(
} }
} }
/// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys). /// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys unter Windows bzw. Swap unter Linux/macOS).
pub fn lock_memory(ptr: *const u8, len: usize) -> bool { pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
#[cfg(windows)] #[cfg(windows)]
{ {
@@ -539,7 +635,17 @@ pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
} }
unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 } unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 }
} }
#[cfg(not(windows))] #[cfg(unix)]
{
extern "C" {
fn mlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int;
}
if ptr.is_null() || len == 0 {
return false;
}
unsafe { mlock(ptr as *const std::ffi::c_void, len) == 0 }
}
#[cfg(not(any(windows, unix)))]
{ {
let _ = (ptr, len); let _ = (ptr, len);
false false
@@ -558,7 +664,17 @@ pub fn unlock_memory(ptr: *const u8, len: usize) -> bool {
} }
unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 } unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 }
} }
#[cfg(not(windows))] #[cfg(unix)]
{
extern "C" {
fn munlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int;
}
if ptr.is_null() || len == 0 {
return false;
}
unsafe { munlock(ptr as *const std::ffi::c_void, len) == 0 }
}
#[cfg(not(any(windows, unix)))]
{ {
let _ = (ptr, len); let _ = (ptr, len);
false false
+152
View File
@@ -0,0 +1,152 @@
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use bytes::Bytes;
use dav_server::{
davpath::DavPath,
fs::{DavFileSystem, OpenOptions},
};
use rand::RngCore;
use sanctum::{
crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION},
storage::Database,
verify::verify_container,
vfs::SanctumFs,
};
/// Live-Crash- und Stresstest: Simuliert harten Verbindungsabbruch und Power-Cut
/// während intensiver paralleler Schreibvorgänge im VFS.
#[tokio::test]
async fn test_live_crash_and_recovery_stress() {
let temp_dir = std::env::temp_dir();
let container_path: PathBuf = temp_dir.join(format!("sanctum_live_stress_{}.sanctum", std::process::id()));
if container_path.exists() {
let _ = std::fs::remove_file(&container_path);
}
let password = "LiveStressPassword2026!";
let salt = generate_salt();
let kdf_params = KdfParams {
memory_cost: 1024,
time_cost: 1,
parallelism: 1,
};
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation");
let dek = generate_dek();
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("DEK wrapping");
// 1. Initialisierung des Containers
{
let db = Database::open(&container_path).expect("Open database");
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
.expect("Init schema");
db.checkpoint().expect("Initial Checkpoint");
}
// 2. Parallele Schreiblast mit SanctumFs erzeugen
let stop_signal = Arc::new(AtomicBool::new(false));
let db = Database::open(&container_path).expect("Open database for VFS");
let fs = SanctumFs::new(db, dek.clone(), FORMAT_VERSION);
let mut handles = Vec::new();
// Spawn 4 parallele Schreiber
for worker_id in 0..4 {
let fs_clone = fs.clone();
let stop_clone = stop_signal.clone();
let handle = tokio::spawn(async move {
let mut file_idx = 0;
while !stop_clone.load(Ordering::Relaxed) && file_idx < 10 {
let file_path_str = format!("/worker_{}_file_{}.dat", worker_id, file_idx);
let dav_path = DavPath::new(&file_path_str).unwrap();
let mut opts = OpenOptions::default();
opts.write = true;
opts.create = true;
opts.truncate = true;
// Datei anlegen
let mut file = match fs_clone.open(&dav_path, opts).await {
Ok(f) => f,
Err(_) => break,
};
// Mehrere 256-KB Blöcke schreiben (über mehrere Chunks hinweg)
let mut payload = vec![0u8; 256 * 1024];
rand::thread_rng().fill_bytes(&mut payload);
for _ in 0..6 {
if stop_clone.load(Ordering::Relaxed) {
break;
}
let _ = file.write_bytes(Bytes::copy_from_slice(&payload)).await;
}
let _ = file.flush().await;
file_idx += 1;
}
});
handles.push(handle);
}
// Lass die Worker 500ms unter Volllast schreiben
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// 3. Simuliere abrupten Prozessabbruch (Hard Kill / Power Cut)
// Wir brechen die Worker hart ab (Cancel) und verwerfen das FS-Handle ohne sauberen Unmount
stop_signal.store(true, Ordering::SeqCst);
for h in handles {
h.abort(); // Simuliert Kill
}
// FS ohne Checkpoint/Drop-Finalisierung freigeben
drop(fs);
// 4. Recovery & Integritätsprüfung nach Crash
// Das System muss die SQLite WAL-Datei automatisch erkennen und verarbeiten
let verify_result = verify_container(&container_path, Some(&dek), false).expect("Verify post-crash");
assert!(
verify_result.is_healthy(),
"Container muss nach Crash vollkommen konsistent sein! Fehler: {:?}",
verify_result.errors
);
assert_eq!(verify_result.corrupted_chunks, 0, "Keine korrupten Chunks erlaubt");
// 5. Konsistentes Weiterarbeiten nach dem Absturz
let db_recovered = Database::open(&container_path).expect("Open database after crash");
let fs_recovered = SanctumFs::new(db_recovered, dek.clone(), FORMAT_VERSION);
// Neue Datei im wiederhergestellten Dateisystem anlegen und lesen
let recovery_test_path = DavPath::new("/post_crash_verification.txt").unwrap();
{
let mut opts = OpenOptions::default();
opts.write = true;
opts.create = true;
opts.truncate = true;
let mut file = fs_recovered
.open(&recovery_test_path, opts)
.await
.expect("Create post-crash file");
file.write_bytes(Bytes::from_static(b"Sanctum Crash Consistency Verified!"))
.await
.expect("Write post crash file");
file.flush().await.expect("Flush post crash file");
}
// Datei wieder einlesen
{
let mut opts = OpenOptions::default();
opts.read = true;
let mut file = fs_recovered
.open(&recovery_test_path, opts)
.await
.expect("Read post-crash file");
let bytes = file.read_bytes(1024).await.expect("Read bytes");
assert_eq!(&bytes[..], b"Sanctum Crash Consistency Verified!");
}
// Sauberes Aufräumen der Testdatei
drop(fs_recovered);
let _ = std::fs::remove_file(&container_path);
}