Compare commits
40
Commits
v0.1.0
...
3f3f8f2730
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f3f8f2730 | ||
|
|
650cfc7cd4 | ||
|
|
9d1a4d3ad7 | ||
|
|
aa25719c0d | ||
|
|
34378998c4 | ||
|
|
4ef0c414fa | ||
|
|
1f3afb1e52 | ||
|
|
5f2040f8bf | ||
|
|
da2ddbd6ab | ||
|
|
46f976b353 | ||
|
|
6cb48f55d9 | ||
|
|
7ed0f51fb1 | ||
|
|
dae0b3b7c3 | ||
|
|
541190cff4 | ||
|
|
99813ae2a3 | ||
|
|
90cf4927af | ||
|
|
abf395627a | ||
|
|
fcd59dfe68 | ||
|
|
a8fdfc674e | ||
|
|
70d03df676 | ||
|
|
771c08ff0f | ||
|
|
98bfac718f | ||
|
|
030ce6a1e5 | ||
|
|
b8e4dcb614 | ||
|
|
ea571d245e | ||
|
|
bb4268cb62 | ||
|
|
952e0cb23a | ||
|
|
fb154c0d2e | ||
|
|
5c1ac89989 | ||
|
|
1e6854e9f5 | ||
|
|
2cb065c8c0 | ||
|
|
38df3d3845 | ||
|
|
1c8a860184 | ||
|
|
2d14c64c3e | ||
|
|
a9c3dd25a3 | ||
|
|
58336fc059 | ||
|
|
83dc42dd86 | ||
|
|
38a8a747c7 | ||
|
|
38c0a6dc12 | ||
|
|
d15ebd8916 |
@@ -2,3 +2,7 @@
|
||||
/dist
|
||||
*.sanctum
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
*.token
|
||||
.token
|
||||
|
||||
+116
-1
@@ -5,7 +5,122 @@ 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/)
|
||||
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.4.1] - 2026-09-14
|
||||
|
||||
### Fixed
|
||||
- **Linux Mount-Stabilität & Signal-Handling**:
|
||||
- Behebung eines sofortigen automatischen Aushängens direkt nach erfolgreichem Mount unter Linux: Windows-spezifische Kontrollkanäle (Console-Close, Session-Lock, System-Tray) wurden auf Nicht-Windows-Systemen sofort geschlossen, wodurch `tokio::select!` über `None` vorzeitig ausgelöst wurde.
|
||||
- Plattform-spezifische Aufteilung der `tokio::select!`-Ereignisschleife: Unter Linux/Unix wird nun sauber auf POSIX-Terminierungssignale (`SIGINT`, `SIGTERM`, `SIGHUP`) gelauscht.
|
||||
- Defensives Pattern-Matching mit `Some(()) = rx.recv()` stellt sicher, dass geschlossene Kanäle keine unberechtigten Shutdowns mehr auslösen können.
|
||||
- Automatisches Öffnen via `xdg-open` auf Linux vorerst unterdrückt.
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
- **Human Factors & OpSec-UX (HF-01 bis HF-04)**:
|
||||
- Maskierte interaktive Notfallschlüssel-Eingabe: `--recovery-key` ohne CLI-Argument liest die 24 BIP-39-Wörter via `rpassword` ein, ohne Klartextspuren in der PowerShell-Historie (`ConsoleHost_history.txt`) zu hinterlassen.
|
||||
- BIP-39 Resilienz & Tippfehler-Diagnose: Automatische String-Normalisierung (Kleinschreibung, Whitespace- und Satzzeichenbereinigung) sowie Levenshtein-Kandidatenvorschläge ($\le 2$ Distanz) bei Eingabefehlern.
|
||||
- Stealth-Mount Modus (`--stealth` / `-s`): Vollständig lautloser Betrieb ohne Banner, Token-Ausgabe, Mount-Pfade oder Box-Rahmen.
|
||||
- VFS Carrier-Schutz im Decoy-Vault: Umfassender Lösch-, Schreib-, Truncate- und Umbenennungsschutz der Alibi-Trägerdatei (`FsError::Forbidden`) bei Zugriff über Slot 0.
|
||||
- **Chaos Engineering & Crash-Resilienz (CHAOS-01 bis CHAOS-03)**:
|
||||
- Transaktionale Crash-Consistency (CHAOS-01): Bündelung von Chunk-Write und Dateigrößen-Aktualisierung (`write_chunk_and_update_size`) in einer einzigen atomaren SQLite-Transaktion (`tx.commit()`) für vollständige Resilienz gegen plötzlichen Stromausfall.
|
||||
- Path Fuzzing & Injection Guard (CHAOS-02): Strikte Validierung aller VFS-Pfade und Knotennamen gegen Null-Bytes (`\0`) und ASCII-Steuerzeichen ($< 0x20$).
|
||||
- Fault Recovery bei Disk Full (CHAOS-03): Sofortige Invalidierung des In-Memory-Chunk-Caches bei Schreibfehlern zur Vermeidung kaskadierender Drop-Panics bei vollem Zieldatenträger.
|
||||
- **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`.
|
||||
- Windows Defender Controlled Folder Access (CFA / Ransomware-Schutz) Diagnose mit gezielter Hilfestellung.
|
||||
|
||||
### Changed
|
||||
- **Visuelle Parität & Plausible Deniability beim Mounten (HF-01)**:
|
||||
- Beseitigung verräterischer Konsolenausgaben (magenta hervorgehobene Hinweise auf Hidden Vault / Slot 1) und Harmonisierung des Explorer-Verhaltens für beide Safes.
|
||||
- Vereinheitlichung der Ausgaben von `sanctum verify` und `sanctum recovery-key` zur Vermeidung von Rückschlüssen auf vorhandene Hidden-Vault-Slots.
|
||||
- Interaktiver Assistent für `sanctum restore-header` bei Aufruf ohne CLI-Parameter.
|
||||
|
||||
## [0.3.0] - 2026-09-09
|
||||
|
||||
### Added
|
||||
- **Modell A: Steganografischer Carrier & Plausible Deniability Phase 2**:
|
||||
- Zwei-Schichten-AEAD (`DEK_0` + `DEK_1`) zur vollständigen Abwehr von Chunks-Accounting-Angriffen.
|
||||
- Physische Dateigrößen-Invarianz: Versteckte Schreibvorgänge verändern die Host-Dateigröße um exakt 0 Bytes.
|
||||
- Virtuelles Carrier-Dateisystem (`CarrierFs`) mit reserviertem Block 0 für Manifest und Block-Allokationsbitmap.
|
||||
- Schreib-, Lösch- und Umbenennungsschutz der Alibi-Trägerdatei im Decoy-Mount (`FsError::Forbidden`).
|
||||
- **CLI & Disaster Recovery Erweiterungen**:
|
||||
- `sanctum backup` & `sanctum restore`: Dedizierte CLI-Befehle für konsistente Online-Sicherungen.
|
||||
- `sanctum restore-header --slot <0|1>`: Gezielte Wiederherstellung für Decoy- oder Hidden-Vault-Header.
|
||||
- `sanctum init --carrier-file <NAME> --carrier-size <MB>`: Frei konfigurierbare Alibi-Trägerdatei mit forensischen Sicherheitsprüfungen.
|
||||
- **SRE & Production Resilience**:
|
||||
- Nativer Win32 `SetConsoleCtrlHandler`: Synchrones Aushängen und WAL-Checkpointing bei Schließen des Konsolenfensters (`CTRL_CLOSE_EVENT`, `CTRL_LOGOFF_EVENT`, `CTRL_SHUTDOWN_EVENT`).
|
||||
- Stale Mount Self-Healing: Automatische Erkennung und Bereinigung verwaister Windows-Netzlaufwerke (Systemfehler 85).
|
||||
- Automatische Erkennung und Diagnosehilfe für den Windows `WebClient`-Dienst (Fehler 67).
|
||||
- **Red Team & Threat Model Hardening**:
|
||||
- WebDAV Loopback Protection: Dynamisches 128-Bit Session-Token im URL-Pfad (Schutz vor unprivilegierten lokalen Prozessen & CSRF).
|
||||
- Anti-DNS-Rebinding & Anti-Spoofing: Strikte Fail-Closed Host-Header-Validierung.
|
||||
- Anti-Slowloris & Connection Limiting: Begrenzung auf maximal 64 gleichzeitige WebDAV-Verbindungen und 15s Header-Read-Timeout.
|
||||
- Decompression-Bomb-Schutz: Strikter 1-MB-Größen-Guard vor LZ4-Dekomprimierung zur Vermeidung von Speichererschöpfung (CWE-400).
|
||||
- Verzeichnis-Hijacking-Schutz: Dynamische Bindung verschlüsselter Knotennamen an die `parent_id` via AEAD-AAD.
|
||||
- Vollständige RAM-Zeroization: Schutz sensibler Daten im Heap via `zeroize::Zeroize` bei Puffer-Swaps und `Drop`.
|
||||
|
||||
### Changed
|
||||
- Standardname der Trägerdatei auf forensisch plausiblen Typ `system_backup.dat` geändert.
|
||||
- Heap- und Schreiboptimierung: Reduzierung der CSPRNG-Padding-Generierung auf den tatsächlichen Slack-Bereich (~30-fache Schreibbeschleunigung).
|
||||
- Inaktivitäts-Tracking: `self.touch()` reagiert nur noch auf echte I/O-Interaktionen (`open`, read, write) und ignoriert passive Explorer-Hintergrundabfragen (`metadata`, `read_dir`).
|
||||
- LZ4-Kompression: Schwellenwert von mindestens 64 Bytes Ersparnis eingeführt (`compressed.len() + 64 <= plaintext.len()`).
|
||||
|
||||
## [0.2.0] - 2026-09-08
|
||||
|
||||
### Added
|
||||
- **Transparente LZ4-Kompression**:
|
||||
- `lz4_flex` Integration mit Frame-Format vor der AEAD-Verschlüsselung.
|
||||
- Adaptiver Fallback: Chunks werden nur komprimiert, wenn mindestens 64 Bytes gespart werden.
|
||||
- Transparente On-the-Fly Dekompression bei Leseoperationen.
|
||||
- **Disaster Recovery & Integrität**:
|
||||
- BIP-39 Mnemonic Seed Phrases (24 Wörter) für Master-Key Recovery.
|
||||
- `sanctum recovery-key`: Export und Anzeige des BIP-39 Notfallschlüssels.
|
||||
- `sanctum backup` & `sanctum restore`: Konsistente Online-Sicherungen via SQLite Backup API mit WAL-Flush.
|
||||
- `sanctum verify`: Vollständige kryptografische Integritätsprüfung aller AEAD-Chunks und B-Tree-Validierung.
|
||||
- **Windows Explorer Integration & Bedienkomfort**:
|
||||
- `sanctum register` & `sanctum unregister`: Windows-Kontextmenü im Explorer (`HKCU\Software\Classes\.sanctum`, "In Sanctum öffnen", Icon-Zuordnung).
|
||||
- Intelligente Erkennung freier Laufwerksbuchstaben (von Z: abwärts).
|
||||
- Automatisches Öffnen des gemounteten Laufwerks im Windows Explorer.
|
||||
- System-Tray Icon (`tray-icon`) im Windows Infobereich mit Status und schnellem Unmount.
|
||||
- **Explorer Anti-Leak Shield & OpSec**:
|
||||
- Windows Anti-Leak Filter: Blockiert `thumbs.db`, `desktop.ini`, `*.tmp`, Office-Lockfiles (`~$*`) und NTFS Alternative Data Streams (`:Zone.Identifier`).
|
||||
- Activity Tracking & Inaktivitäts-Timeout: Automatischer Unmount bei Leerlauf.
|
||||
- Windows Session Lock Monitor: Registrierung für `WTS_SESSION_LOCK` mit sicherem Auto-Unmount beim Sperren des PCs.
|
||||
- **Storage Compaction & Anti-Forensics**:
|
||||
- SQLite Incremental Auto-Vacuum (`PRAGMA auto_vacuum = INCREMENTAL;`) zur physikalischen Freigabe ungenutzter Dateiseiten an das Host-Dateisystem.
|
||||
- Kryptografisches Chunk-Shredding: Chunks werden vor dem Löschen/Abschneiden mit CSPRNG-Zufallsrauschen überschrieben.
|
||||
- `sanctum compact`: Manuelles oder beim Aushängen automatisches Freigeben von Freelist-Pages.
|
||||
- Plausible Deniability (Hidden Vault): Dual-Slot Header-Architektur. Unbenutzte Slots enthalten ununterscheidbares CSPRNG-Rauschen.
|
||||
- `sanctum init --with-hidden`: Erstellung zweier getrennter Safes (Decoy & Hidden) mit individuellen Passphrasen und BIP-39 Recovery-Karten.
|
||||
- Vollständige Dateinamen-Verschlüsselung für den Hidden Vault.
|
||||
- **Passwort-Verwaltung (`sanctum passwd`)**:
|
||||
- Sofortiges Ändern des Master-Passworts über DEK-Rewrapping mit neuem Argon2id-Salt und KEK-Parametern ohne Neuverschlüsselung der Nutzdaten. Multi-Slot fähig.
|
||||
|
||||
## [0.1.0] - 2026-09-07
|
||||
|
||||
|
||||
Generated
+333
-12
@@ -104,7 +104,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -115,7 +115,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -136,6 +136,12 @@ dependencies = [
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -160,6 +166,33 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.14.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2"
|
||||
dependencies = [
|
||||
"hex-conservative",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
@@ -175,6 +208,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -273,18 +312,82 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -394,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -421,6 +524,33 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-macros"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -596,6 +726,21 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmlescape"
|
||||
version = "0.3.1"
|
||||
@@ -896,6 +1041,24 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
|
||||
dependencies = [
|
||||
"twox-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -935,7 +1098,7 @@ checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -944,7 +1107,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -956,6 +1119,35 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-foundation"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||
dependencies = [
|
||||
"block",
|
||||
"objc",
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_id"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||
dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -974,6 +1166,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "padlock"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1116,7 +1314,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1144,7 +1342,7 @@ checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1154,7 +1352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1163,7 +1361,7 @@ version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.1",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@@ -1179,19 +1377,22 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "sanctum"
|
||||
version = "0.1.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"bip39",
|
||||
"bytes",
|
||||
"clap",
|
||||
"dav-server",
|
||||
"dyn-clone",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"lz4_flex",
|
||||
"rand",
|
||||
"rpassword",
|
||||
"rusqlite",
|
||||
@@ -1201,6 +1402,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tray-item",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1308,7 +1510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1401,6 +1603,21 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
@@ -1415,7 +1632,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1504,6 +1721,28 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tray-item"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59d4bd406170690dc30eabb3badc67a085beaf9b2c3b1923afcc9c26a2191353"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
"libc",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"padlock",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
@@ -1522,6 +1761,15 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@@ -1695,6 +1943,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -1704,6 +1961,70 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.4"
|
||||
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sanctum"
|
||||
version = "0.1.0"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
||||
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
||||
@@ -18,7 +18,7 @@ argon2 = { version = "0.5", features = ["password-hash"] }
|
||||
aes-gcm = { version = "0.10", features = ["zeroize"] }
|
||||
rand = "0.8"
|
||||
zeroize = { version = "1.8", features = ["derive", "zeroize_derive"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled", "backup"] }
|
||||
tokio = { version = "1.40", features = ["full"] }
|
||||
dav-server = { version = "0.11", default-features = false }
|
||||
hyper = { version = "1.4", features = ["server", "http1"] }
|
||||
@@ -33,6 +33,12 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
dyn-clone = "1.0"
|
||||
lz4_flex = "0.11"
|
||||
bip39 = { version = "2.2", features = ["zeroize"] }
|
||||
hex = "0.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tray-item = "0.10"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
+94
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# Rechtliche Hinweise, Compliance & Exportkontrolle (LEGAL)
|
||||
|
||||
Dieses Dokument regelt die rechtlichen Rahmenbedingungen, Exportkontroll-Klassifizierungen, Lizenz-Compliance sowie die datenschutzrechtlichen und haftungsrechtlichen Grundlagen der Software **Sanctum**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Kryptografie-Exportkontrolle & Dual-Use-Klassifizierung
|
||||
|
||||
Sanctum implementiert starke, moderne kryptografische Verfahren mit symmetrischen Schlüssellängen von 256 Bit (AES-256-GCM), asymmetrischer KDF (Argon2id) sowie BIP-39 Schlüsselableitung.
|
||||
|
||||
### A. US Export Administration Regulations (EAR)
|
||||
* **Klassifizierung (ECCN)**:
|
||||
* Hardware/Software mit starker Verschlüsselung fällt unter **ECCN 5A002.a** bzw. **5D002** (Commerce Control List, Category 5, Part 2: *Information Security*).
|
||||
* **Ausnahmetatbestand für frei verfügbaren Quellcode**:
|
||||
* Gemäß **15 C.F.R. § 742.15(b)** (*Publicly Available Encryption Source Code*) und **15 C.F.R. § 734.3(b)(3)** unterliegt öffentlich zugänglicher Open-Source-Quellcode, der jedermann unentgeltlich und ohne Zugriffsbeschränkungen zur Verfügung steht, **nicht** den Ausfuhrgenehmigungspflichten der EAR.
|
||||
* Sanctum wird unter der freien MIT-Lizenz auf frei zugänglichen Repositorien (einschließlich Quellcode, Spezifikationen und Build-Skripten) bereitgestellt.
|
||||
* **Sanktionierte Staaten & Personen (OFAC)**:
|
||||
* Trotz der allgemeinen Open-Source-Freistellung ist der wissentliche direkte oder indirekte Export/Re-Export an Personen oder Entitäten auf US-Sanktionslisten (OFAC Specially Designated Nationals List - SDN) sowie in Länder unter umfassenden Handelsembargos (Kuba, Iran, Nordkorea, Syrien, besetzte Gebiete der Ukraine) untersagt.
|
||||
|
||||
### B. Europäische Union (EU Dual-Use-Verordnung 2021/821)
|
||||
* **Anhang I, Kategorie 5 Teil 2**:
|
||||
* Systeme der Informationssicherheit sind grundsätzlich im Anhang I der Dual-Use-Verordnung gelistet.
|
||||
* **Allgemeine Software-Anmerkung (General Software Note - GSN)**:
|
||||
* Gemäß der GSN zu Anhang I der Verordnung (EU) 2021/821 unterliegt Software **nicht** der Ausfuhrkontrolle, wenn sie:
|
||||
1. Allgemein zugänglich ist (*in the public domain*), z. B. durch uneingeschränkten, unentgeltlichen Download; oder
|
||||
2. Der Mindest-Quellcode für die Installation, den Betrieb oder die Wartung nicht-kontrollierter Güter ist.
|
||||
* Da Sanctum als freie Open-Source-Software jedermann unentgeltlich zur Verfügung steht, greift die Freistellung der GSN.
|
||||
|
||||
---
|
||||
|
||||
## 2. Plausible Deniability & Herausgabepflichten (Key Disclosure Laws)
|
||||
|
||||
Sanctum implementiert mit **Modell A** (Steganografischer Alibi-Carrier im Decoy-Vault mit exakter physikalischer Dateigrößen-Invarianz und uniformem Entropie-Slack) ein mathematisch informationstheoretisches Modell für **Plausible Deniability** (Glaubhafte Abstreitbarkeit).
|
||||
|
||||
> [!WARNING]
|
||||
> **Wichtiger Hinweis für Anwender**: Plausible Deniability ist ein **technischer Schutzmechanismus**, kein juristisches Schutzschild. Die Wirksamkeit gegenüber staatlichen Ermittlungsbehörden hängt maßgeblich von der jeweiligen Jurisdiktion und dem eigenen Aussageverhalten ab:
|
||||
|
||||
1. **Deutschland / Österreich / Schweiz (DACH)**:
|
||||
* **Nemo tenetur se ipsum accusare**: Niemand ist verpflichtet, sich in einem Strafverfahren selbst zu belasten (§ 136 Abs. 1 S. 2 StPO, Art. 6 EMRK). Beschuldigte haben das Recht, Passwörter und Schlüssel vollständig zu verschweigen.
|
||||
* **Aussage als Beschuldigter**: Gibt ein Beschuldigter freiwillig das Passwort für den Decoy-Vault (Slot 0) heraus und verschweigt die Existenz des Hidden Vaults (Slot 1), macht er sich in der Regel nicht strafbar, solange er keine Dritten falsch verdächtigt (§ 164 StGB).
|
||||
* **Zeugenstatus**: Zeugen sind grundsätzlich zur Wahrheit verpflichtet (§ 70 StPO). Bei falscher Aussage droht Strafbarkeit (§§ 153 ff. StGB).
|
||||
2. **Großbritannien (UK - RIPA 2000 Part III, Section 49 & 53)**:
|
||||
* Britische Behörden können unter Androhung einer Freiheitsstrafe von bis zu 2 Jahren (bei Gefährdung der nationalen Sicherheit bis zu 5 Jahren) die Offenlegung von Schlüsseln förmlich anordnen (*Section 49 Notice*).
|
||||
* Da Sanctum dank Modell A keine forensisch nachweisbaren Spuren eines Hidden Vaults hinterlässt, existiert für Ermittler kein Nachweis, dass überhaupt ein zweiter Schlüssel existiert. Dennoch obliegt die Beweiswürdigung dem zuständigen Gericht.
|
||||
3. **Vereinigte Staaten von Amerika (USA - 5th Amendment)**:
|
||||
* Das 5. Zusatzprotokoll schützt vor erzwungenen Zeugenaussagen gegen sich selbst. Passwörter gelten als geschützter Denkinhalt (*testimonial*).
|
||||
* *Foregone Conclusion Doctrine*: Kann der Staat beweisen, dass bestimmte inkriminierte Dateien auf dem Gerät existieren, kann die Passworteingabe als Vollstreckungshandlung erzwungen werden. Bei Sanctum verhindert die ununterscheidbare Entropie des Trägers den Nachweis der Existenz von Daten im Hidden Safe.
|
||||
|
||||
---
|
||||
|
||||
## 3. Haftungsausschluss & Gewährleistung (EU / DACH Recht)
|
||||
|
||||
Die in der [LICENSE](LICENSE) enthaltene US-Standardklausel (*„AS IS, WITHOUT WARRANTY OF ANY KIND“*) wird für den Geltungsbereich des europäischen und deutschen Rechts wie folgt präzisiert und ergänzt:
|
||||
|
||||
### A. Schenkungsrechtliche Haftungsbeschränkung (§ 521 BGB)
|
||||
1. Sanctum wird dem Anwender unentgeltlich und ohne Gegenleistung überlassen.
|
||||
2. Gemäß **§ 521 des Bürgerlichen Gesetzbuches (BGB)** haftet der Urheber / Entwickler bei unentgeltlicher Softwareüberlassung **nur für Vorsatz und grobe Fahrlässigkeit**.
|
||||
3. Die Haftung für einfache/leichte Fahrlässigkeit, mittelbare Schäden, Folgeschäden, Datenverluste, entgangenen Gewinn oder Betriebsunterbrechungen ist im gesetzlich zulässigen Rahmen vollständig ausgeschlossen.
|
||||
|
||||
### B. Eigenverantwortung für Backups & Notfallschlüssel
|
||||
* Kryptografie verzeiht keine Fehler: Bei Verlust beider Passwörter sowie der 24-Wort BIP-39 Notfallschlüssel ist eine Entschlüsselung mathematisch ausgeschlossen. Der Entwickler verfügt über keinerlei Master-Keys, Backdoors oder Wiederherstellungsmechanismen.
|
||||
* Der Anwender ist für die regelmäßige externe Sicherung seiner Container (`sanctum backup`) und das sichere Verwahren seiner BIP-39 Notfallkarten allein verantwortlich.
|
||||
|
||||
---
|
||||
|
||||
## 4. Datenschutz & DSGVO / GDPR (Zero-Telemetry-Garantie)
|
||||
|
||||
Sanctum folgt uneingeschränkt dem Prinzip **Privacy by Design and by Default** (Art. 25 DSGVO):
|
||||
|
||||
* **0 Bytes Telemetrie**: Sanctum enthält keinen Code für Analytics, Fehlerberichterstattung (Crash Reporting), User-Tracking oder Telemetrie.
|
||||
* **100% Offline-Betrieb**: Sanctum stellt zu keinem Zeitpunkt ausgehende Internetverbindungen her. Es gibt keine Update-Prüfungen, Pingbacks oder Cloud-Synchronisationen.
|
||||
* **Keine Speicherung personenbezogener Daten**: Sanctum speichert lokal keinerlei Protokolle über eingegebene Passwörter, Benutzer-IDs oder Dateinamen außerhalb des verschlüsselten Containers.
|
||||
* **Windows Host-Hygiene**: Durch das integrierte *Anti-Leak-Shield* und den *RAM-Paging-Schutz* (`VirtualLock`) werden Spuren im Host-System (Thumbs.db, temporäre Dateien, Auslagerungsdatei) aktiv unterbunden.
|
||||
+113
@@ -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.
|
||||
```
|
||||
@@ -1,42 +1,60 @@
|
||||
<p align="center">
|
||||
<img src="assets/logo.png" alt="Sanctum Logo" width="220"/>
|
||||
</p>
|
||||
|
||||
# Sanctum 🛡️
|
||||
|
||||
Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendung in Rust, die einen verschlüsselten Ein-Datei-Container (`.sanctum`) unter Windows (10/11) im reinen Userland verwaltet.
|
||||
**Verschlüsselter Ein-Datei-Container unter Windows 10/11 im reinen Userland**
|
||||
|
||||
- **Keine Administratorrechte erforderlich**
|
||||
- **Keine Kernel-Treiber** (weder WinFsp noch Dokan)
|
||||
- **Transparenter Windows Explorer-Zugriff** über einen integrierten lokalen WebDAV-Server (`127.0.0.1`) via Windows-Bordmittel (`net use`)
|
||||
- **Statisches Single-Binary** (`sanctum.exe`, ~4.6 MB) ohne externe DLL-Abhängigkeiten
|
||||
Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (`.sanctum`) unter Windows 10/11 im reinen Userland verwaltet.
|
||||
|
||||
- **100% Userland**: Keine Administratorrechte erforderlich, keine Kernel-Treiber (weder WinFsp noch Dokan).
|
||||
- **Windows Explorer-Integration**: Einhängen als nativer Netzlaufwerk-Buchstabe (Z: abwärts) via lokalem WebDAV (`127.0.0.1`), Explorer-Kontextmenü und System-Tray-Icon.
|
||||
- **Transparente LZ4-Kompression**: Automatische Kompression von Chunks mit On-the-Fly-Dekompression und adaptivem Fallback.
|
||||
- **Anti-Forensik & Storage Compaction**: Inkrementelles Auto-Vacuum zur physikalischen Freigabe von Speicherplatz und kryptografisches Überschreiben (Shredding) von Datenblöcken mit CSPRNG-Rauschen vor dem Löschen.
|
||||
- **Plausible Deniability (Hidden Safe)**: Dual-Slot-Header. Ein unbenutzter Slot enthält uniformes CSPRNG-Rauschen, das sich nicht von einem echten Hidden Vault unterscheiden lässt. Volle Dateinamen-Verschlüsselung im Hidden Vault.
|
||||
- **OpSec & Explorer Anti-Leak Shield**: Blockiert Explorer-Spuren (`Thumbs.db`, `desktop.ini`, `*.tmp`, `:Zone.Identifier`), automatischer Unmount bei Inaktivität oder Windows-Sitzungssperre (`Win + L`).
|
||||
- **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.
|
||||
|
||||
> 💡 **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
|
||||
|
||||
- **Key Derivation (Argon2id)**:
|
||||
- **Schlüsselableitung (Argon2id)**:
|
||||
Aus dem Master-Passwort wird mittels `Argon2id` ($M=64\,\text{MB}, T=3, P=4$) ein 256-Bit Key Encryption Key (KEK) abgeleitet.
|
||||
- **Data Encryption Key (DEK)**:
|
||||
Ein zufälliger 256-Bit Schlüssel via CSPRNG (`OsRng`). Der DEK wird mit dem KEK via AES-256-GCM verschlüsselt und im Header abgelegt.
|
||||
- **RAM-Schutz**:
|
||||
Alle Schlüsselstrukturen implementieren das `Zeroize`-Trait (`Zeroizing<[u8; 32]>`), um sensible Schlüsseldaten beim Verlassen des Gültigkeitsbereichs im Arbeitsspeicher sicher zu nullen.
|
||||
Zufälliger 256-Bit Schlüssel via CSPRNG (`OsRng`). Der DEK wird mit dem KEK via AES-256-GCM verschlüsselt und im Header abgelegt.
|
||||
- **Speichersicherheit (Zeroize)**:
|
||||
Alle Schlüsselstrukturen implementieren das `Zeroize`-Trait (`Zeroizing<[u8; 32]>`), um sensible Schlüsseldaten beim Verlassen des Gültigkeitsbereichs im RAM sofort sicher zu nullen.
|
||||
- **Chunk-Verschlüsselung (AES-256-GCM)**:
|
||||
Dateien werden in Blöcken von 1 MB verschlüsselt.
|
||||
- **Schutz vor Swap-Angriffen**:
|
||||
Als Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) fest eingebunden. Dadurch wird verhindert, dass Chunks zwischen Dateien oder innerhalb einer Datei vertauscht werden können.
|
||||
- **Swap-Attack-Schutz**:
|
||||
Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen von Chunks zwischen Dateien oder innerhalb einer Datei führt zum Authentifizierungsfehler.
|
||||
- **Plausible Deniability (Multi-Slot)**:
|
||||
Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Hidden Vault oder CSPRNG-Dummy. Ein Angreifer kann mathematisch nicht feststellen, ob Slot 1 ungenutzt ist oder einen zweiten Tresor birgt.
|
||||
- **Dateinamen-Verschlüsselung**:
|
||||
Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert.
|
||||
- **Kryptografisches Chunk-Shredding**:
|
||||
Vor jedem Löschen oder Kürzen werden Chunk-Payloads in der SQLite-Datenbank mit CSPRNG-Rauschen überschrieben.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Storage Engine (SQLite3 im VFS-Container)
|
||||
## 📦 Speicher- & Kompressions-Engine
|
||||
|
||||
Der gesamte Container besteht aus exakt **einer** Datei auf der Host-Festplatte (`.sanctum`), die dynamisch bis über 100 GB wachsen kann.
|
||||
Der Container besteht aus exakt **einer** Host-Datei (`.sanctum`), die dynamisch bis über 100 GB wachsen kann:
|
||||
|
||||
- **Pragmas**:
|
||||
- **SQLite3 WAL & Auto-Vacuum**:
|
||||
- `PRAGMA auto_vacuum = INCREMENTAL;`
|
||||
- `PRAGMA journal_mode = WAL;`
|
||||
- `PRAGMA synchronous = NORMAL;`
|
||||
- `PRAGMA page_size = 8192;`
|
||||
- `PRAGMA foreign_keys = ON;`
|
||||
- **Tabellen**:
|
||||
- `meta`: Container-Header mit Magic Bytes (`SANCTUM\0`), Version 1, Salt, KDF-Parametern und Wrapped DEK.
|
||||
- `nodes`: Verzeichnis- und Dateiknoten mit Hierarchiebaum und Zeitstempeln.
|
||||
- `chunks`: Verschlüsselte Nutzdatenblöcke mit Nonce und Authentifizierungstag.
|
||||
- **LZ4-Kompression**:
|
||||
Chunks werden vor der Verschlüsselung via `lz4_flex` komprimiert. Spart die Kompression weniger als 64 Bytes (z. B. bei bereits komprimierten Bildern oder Videos), wird adaptiv die Rohform verschlüsselt.
|
||||
- **Speicherplatzrückgabe**:
|
||||
Durch `PRAGMA auto_vacuum = INCREMENTAL;` können freigewordene SQLite-Pages beim Aushängen oder via `sanctum compact` vollständig an das Windows-Hostdateisystem zurückgegeben werden.
|
||||
|
||||
---
|
||||
|
||||
@@ -52,42 +70,171 @@ Das fertige Binary befindet sich unter `target/release/sanctum.exe`.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Verwendung
|
||||
## 🛠️ CLI-Referenz & Verwendung
|
||||
|
||||
### 1. Container initialisieren
|
||||
|
||||
### 1. Neuen Container anlegen
|
||||
```powershell
|
||||
sanctum.exe init --path "C:\Users\username\Documents\safe.sanctum"
|
||||
```
|
||||
# Standard-Container anlegen:
|
||||
sanctum.exe init --path "C:\Pfad\tresor.sanctum"
|
||||
|
||||
# Container mit Plausible Deniability (Decoy + Hidden Safe) anlegen:
|
||||
sanctum.exe init --path "C:\Pfad\tresor.sanctum" --with-hidden
|
||||
```
|
||||
*(Gibt nach Passworteingabe eine 24-Wort BIP-39 Notfall-Wiederherstellungskarte aus).*
|
||||
|
||||
---
|
||||
|
||||
### 2. Container einbinden (Mount)
|
||||
|
||||
### 2. Container als Windows-Laufwerk einbinden
|
||||
```powershell
|
||||
sanctum.exe mount --path "C:\Users\username\Documents\safe.sanctum" --drive S
|
||||
```
|
||||
Das Netzlaufwerk `S:` steht sofort im Windows Explorer zur Verfügung.
|
||||
Zum Beenden und sicheren Trennen einfach `Ctrl+C` im Terminal drücken.
|
||||
# Automatische Wahl des nächsten freien Laufwerksbuchstabens (z. B. Z:):
|
||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum"
|
||||
|
||||
# Bestimmten Laufwerksbuchstaben erzwingen:
|
||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S
|
||||
|
||||
# Mit Inaktivitäts-Timeout (in Sekunden):
|
||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --idle-timeout 300
|
||||
|
||||
# Lautloser Stealth-Modus (keine Banner/Token-Ausgabe, kein Explorer-Auto-Open):
|
||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --stealth
|
||||
|
||||
# Explorer-Auto-Open gezielt unterdrücken (maximale ShellBag-OpSec):
|
||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --no-open
|
||||
```
|
||||
- **Automatischer Slot-Unlock**: Sanctum prüft das eingegebene Passwort gegen alle Slots und bindet automatisch den entsprechenden Tresor ein (Slot 0 Decoy oder Slot 1 Hidden Vault).
|
||||
- **Windows Explorer**: Das gemountete Laufwerk wird standardmäßig im Explorer geöffnet (abschaltbar via `--no-open` oder `--stealth`).
|
||||
- **System-Tray**: Ein Schild-Icon im Windows Infobereich erlaubt Statusabfrage und direktes Aushängen.
|
||||
- **Beenden**: `Ctrl+C` im Terminal oder Rechtsklick im Tray -> "Aushängen & Beenden" führt einen sauberen Unmount, Speicher-Compaction und WAL-Checkpoint durch.
|
||||
|
||||
---
|
||||
|
||||
### 3. Container manuell trennen
|
||||
|
||||
### 3. Laufwerk manuell trennen
|
||||
```powershell
|
||||
sanctum.exe unmount --drive S
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Release Packaging
|
||||
### 4. Master-Passwort ändern
|
||||
|
||||
Um ein vollständiges Release-Paket mit Tests, komprimiertem ZIP-Archiv und SHA-256 Prüfsummen zu erstellen:
|
||||
```powershell
|
||||
sanctum.exe passwd --path "C:\Pfad\tresor.sanctum"
|
||||
```
|
||||
Ändert das Passwort über Key-Rewrapping in Sekundenbruchteilen, ohne die Nutzdaten neu verschlüsseln zu müssen.
|
||||
|
||||
---
|
||||
|
||||
### 5. Disaster Recovery & Notfallschlüssel
|
||||
|
||||
```powershell
|
||||
# 24-Wort BIP-39 Notfallschlüssel anzeigen:
|
||||
sanctum.exe recovery-key --path "C:\Pfad\tresor.sanctum"
|
||||
|
||||
# Konsistentes Online-Backup erstellen (auch während Mount möglich):
|
||||
sanctum.exe backup --path "C:\Pfad\tresor.sanctum" --output "D:\Backup\tresor_backup.sanctum"
|
||||
|
||||
# Container aus Backup wiederherstellen:
|
||||
sanctum.exe restore --path "D:\Backup\tresor_backup.sanctum" --output "C:\Pfad\tresor_restored.sanctum"
|
||||
|
||||
# Vollständige Integritätsprüfung (B-Tree, Knoten und AEAD-Tags aller Chunks):
|
||||
sanctum.exe verify --path "C:\Pfad\tresor.sanctum"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Storage Compaction (Speicherbereinigung)
|
||||
|
||||
```powershell
|
||||
# Freelist-Pages an Windows freigeben und Container verkleinern:
|
||||
sanctum.exe compact --path "C:\Pfad\tresor.sanctum"
|
||||
|
||||
# Nur eine begrenzte Anzahl Pages freigeben:
|
||||
sanctum.exe compact --path "C:\Pfad\tresor.sanctum" --pages 500
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Windows Explorer Kontextmenü-Integration
|
||||
|
||||
```powershell
|
||||
# .sanctum-Dateien für Rechtsklick ("In Sanctum öffnen") in HKCU registrieren (ohne Adminrechte):
|
||||
sanctum.exe register
|
||||
|
||||
# Registrierung wieder rückstandslos entfernen:
|
||||
sanctum.exe unregister
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
||||
|
||||
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
||||
|
||||
1. **Anti-Leak Dateifilter**: Unterdrückt das Anlegen von `Thumbs.db`, `desktop.ini`, Office-Sperrdateien (`~$*`), temporären Dateien (`*.tmp`) und NTFS Alternate Data Streams (`:Zone.Identifier`).
|
||||
2. **Inactivity Auto-Lock**: Erkennt Inaktivität anhand echter Lese-/Schreibzugriffe und trennt den Container automatisch nach Erreichen des Timeouts.
|
||||
3. **Session Lock Detection**: Reagiert über `WTSRegisterSessionNotification` sofort auf Windows-Sitzungssperren (`Win + L`) oder Abmeldungen und schließt den Container blitzschnell ab.
|
||||
4. **RAM-Paging-Schutz (`VirtualLock`)**: Verriegelt sensible Schlüsselstrukturen (`DEK`) im physischen RAM, um das Auslagern in `pagefile.sys` oder `swapfile.sys` durch den Windows Memory Manager zu verhindern.
|
||||
5. **ShellBag & Forensik-OpSec**: Über die Optionen `--no-open` oder `--stealth` kann das automatische Öffnen des Windows Explorers vollständig unterdrückt werden, um zu verhindern, dass Ordnernamen persistent im ShellBag-Cache (`UsrClass.dat`) protokolliert werden.
|
||||
6. **VFS Carrier-Schutz**: Die im Decoy-Tresor sichtbare Alibi-Trägerdatei wird im VFS gegen versehentliches Löschen, Überschreiben oder Umbenennen geschützt.
|
||||
7. **Power-Loss Crash-Consistency**: Atomare Bündelung von Block-Schreiboperationen und Metadaten-Größenaktualisierungen in einer SQLite-Transaktion verhindert Datenkorruption bei plötzlichem Stromausfall.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Windows-Kompatibilität & AV/EDR-Tuning
|
||||
|
||||
### Windows Defender SmartScreen ("Unbekannter Herausgeber")
|
||||
Wird die Binärdatei über einen Webbrowser heruntergeladen, versieht Windows sie mit dem *Mark-of-the-Web* (MotW). Da Open-Source-Projekte über kein kostenpflichtiges EV-Code-Signing-Zertifikat verfügen, zeigt Windows beim ersten Start den blauen SmartScreen-Dialog:
|
||||
- **Einmalige Freigabe im Dialog:** Klick auf **"Weitere Informationen"** $\to$ **"Trotzdem ausführen"**.
|
||||
- **Schnellfreigabe via PowerShell:**
|
||||
```powershell
|
||||
Unblock-File .\sanctum.exe
|
||||
```
|
||||
|
||||
### Windows Defender "Überwachter Ordnerzugriff" (Controlled Folder Access - CFA)
|
||||
Ist der erweiterte Ransomware-Schutz von Windows aktiv, kann Defender den Schreibzugriff auf Containerdateien in Standard-Benutzerordnern (`Dokumente`, `Desktop`) mit Fehler 5 (*Zugriff verweigert*) blockieren:
|
||||
- **Abhilfe:** Erlauben Sie `sanctum.exe` in den Windows-Sicherheitseinstellungen (*Viren- & Bedrohungsschutz* $\to$ *Ransomware-Schutz verwalten* $\to$ *App durch überwachten Ordnerzugriff zulassen*), oder speichern Sie Container in einem separaten Verzeichnis (z. B. `C:\Sanctum\`).
|
||||
|
||||
### I/O-Performance-Tuning
|
||||
Windows Defender scannt standardmäßig jeden modifizierten 1-MB-Chunk in Echtzeit. Durch das Hinzufügen der Dateiendung `*.sanctum` zu den Defender-Ausschlüssen lässt sich der I/O-Durchsatz bei großen Kopiervorgängen um 30–50% steigern.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Release Packaging & Distribution
|
||||
|
||||
Um ein Distributionspaket mit Release-Binary, Checksummen und Dokumentation zu bauen:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\package-release.ps1
|
||||
```
|
||||
|
||||
Das fertige Paket liegt in `dist/` bereit:
|
||||
- `dist/sanctum-v0.1.0-windows-x86_64.zip`
|
||||
Erzeugt:
|
||||
- `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`
|
||||
|
||||
---
|
||||
|
||||
## ⚖️ Rechtliche Hinweise, Compliance & Exportkontrolle
|
||||
|
||||
- **Kryptografie-Exportkontrolle (EAR & EU Dual-Use)**:
|
||||
Sanctum nutzt starke Kryptografie (AES-256-GCM, Argon2id). Da der Quellcode als freie Open-Source-Software jedermann unbeschränkt und unentgeltlich zur Verfügung steht, unterliegt er den Ausnahmetatbeständen gem. **15 C.F.R. § 742.15(b)** (US EAR) sowie der **General Software Note (GSN)** der Verordnung (EU) 2021/821.
|
||||
- **Plausible Deniability & Herausgabepflichten**:
|
||||
Sanctum bietet mit dem steganografischen Alibi-Carrier (Modell A) ein informationstheoretisches Modell für glaubhafte Abstreitbarkeit. Dies ist ein technischer Schutzmechanismus, kein juristisches Schutzschild. Die rechtliche Bewertung im Fall strafprozessualer Herausgabeanordnungen (*Key Disclosure*) hängt von der jeweiligen Jurisdiktion ab (z. B. Nemo-tenetur-Grundsatz in DACH vs. RIPA Section 49 im UK).
|
||||
- **Datenschutz & Telemetrie-Freiheit (DSGVO / GDPR)**:
|
||||
Sanctum arbeitet zu 100% offline und enthält keinerlei Code für Telemetrie, Analytics, Fehlerberichterstattung oder IP-Übertragungen (Art. 25 DSGVO *Privacy by Design*).
|
||||
- **Haftungsbeschränkung (EU/DACH Recht)**:
|
||||
Die Software wird als unentgeltliche FOSS-Software bereitgestellt. Gemäß **§ 521 BGB** (Schenkungsrecht) ist die Haftung des Entwicklers auf Vorsatz und grobe Fahrlässigkeit beschränkt. Der Anwender ist für die sichere Verwahrung von Schlüsseln und Backups selbst verantwortlich.
|
||||
- Ausführliche rechtliche Erläuterungen finden sich in [`LEGAL.md`](LEGAL.md).
|
||||
|
||||
---
|
||||
|
||||
## 📄 Lizenz & Changelog
|
||||
|
||||
- Lizenziert unter der [MIT License](LICENSE).
|
||||
- Vollständige Third-Party-Attributionen aller ~230 Abhängigkeiten (Apache-2.0, MIT, BSD, SQLite Public Domain) sind in [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md) dokumentiert.
|
||||
- Ausführliche rechtliche Bestimmungen und Exportkontroll-Hinweise sind in [`LEGAL.md`](LEGAL.md) geregelt.
|
||||
- Details zu allen Versionen und Änderungen findest du im [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
# Third-Party Software Licenses & Attributions
|
||||
|
||||
Sanctum incorporates open-source software packages statically compiled into the binary.
|
||||
All included components are distributed under permissive open-source licenses.
|
||||
This document provides full attribution and license texts in compliance with
|
||||
Section 4 of the Apache License, Version 2.0, the MIT License, and BSD licenses.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dependency Inventory
|
||||
|
||||
| Crate | Version | License |
|
||||
| :--- | :--- | :--- |
|
||||
| `aead` | 0.5.2 | MIT OR Apache-2.0 |
|
||||
| `aes` | 0.8.4 | MIT OR Apache-2.0 |
|
||||
| `aes-gcm` | 0.10.3 | Apache-2.0 OR MIT |
|
||||
| `ahash` | 0.8.12 | MIT OR Apache-2.0 |
|
||||
| `aho-corasick` | 1.1.5 | Unlicense OR MIT |
|
||||
| `android_system_properties` | 0.1.6 | MIT OR Apache-2.0 |
|
||||
| `anstream` | 1.0.0 | MIT OR Apache-2.0 |
|
||||
| `anstyle` | 1.0.14 | MIT OR Apache-2.0 |
|
||||
| `anstyle-parse` | 1.0.0 | MIT OR Apache-2.0 |
|
||||
| `anstyle-query` | 1.1.5 | MIT OR Apache-2.0 |
|
||||
| `anstyle-wincon` | 3.0.11 | MIT OR Apache-2.0 |
|
||||
| `anyhow` | 1.0.104 | MIT OR Apache-2.0 |
|
||||
| `argon2` | 0.5.3 | MIT OR Apache-2.0 |
|
||||
| `arrayvec` | 0.7.8 | MIT OR Apache-2.0 |
|
||||
| `atomic-waker` | 1.1.2 | Apache-2.0 OR MIT |
|
||||
| `autocfg` | 1.5.1 | Apache-2.0 OR MIT |
|
||||
| `base64` | 0.22.1 | MIT OR Apache-2.0 |
|
||||
| `base64ct` | 1.8.3 | Apache-2.0 OR MIT |
|
||||
| `bip39` | 2.2.2 | CC0-1.0 |
|
||||
| `bitcoin_hashes` | 0.14.101 | CC0-1.0 |
|
||||
| `bitflags` | 2.13.1 | MIT OR Apache-2.0 |
|
||||
| `bitflags` | 1.3.2 | MIT/Apache-2.0 |
|
||||
| `blake2` | 0.10.6 | MIT OR Apache-2.0 |
|
||||
| `block` | 0.1.6 | MIT |
|
||||
| `block-buffer` | 0.10.4 | MIT OR Apache-2.0 |
|
||||
| `bumpalo` | 3.20.3 | MIT OR Apache-2.0 |
|
||||
| `bytes` | 1.12.1 | MIT |
|
||||
| `cc` | 1.4.5 | MIT OR Apache-2.0 |
|
||||
| `cfg-if` | 1.0.4 | MIT OR Apache-2.0 |
|
||||
| `chrono` | 0.4.45 | MIT OR Apache-2.0 |
|
||||
| `cipher` | 0.4.4 | MIT OR Apache-2.0 |
|
||||
| `clap` | 4.6.6 | MIT OR Apache-2.0 |
|
||||
| `clap_builder` | 4.6.6 | MIT OR Apache-2.0 |
|
||||
| `clap_derive` | 4.6.4 | MIT OR Apache-2.0 |
|
||||
| `clap_lex` | 1.1.0 | MIT OR Apache-2.0 |
|
||||
| `cocoa` | 0.25.0 | MIT OR Apache-2.0 |
|
||||
| `cocoa-foundation` | 0.1.2 | MIT OR Apache-2.0 |
|
||||
| `colorchoice` | 1.0.5 | MIT OR Apache-2.0 |
|
||||
| `core-foundation` | 0.9.4 | MIT OR Apache-2.0 |
|
||||
| `core-foundation-sys` | 0.8.7 | MIT OR Apache-2.0 |
|
||||
| `core-graphics` | 0.23.2 | MIT OR Apache-2.0 |
|
||||
| `core-graphics-types` | 0.1.3 | MIT OR Apache-2.0 |
|
||||
| `cpufeatures` | 0.2.17 | MIT OR Apache-2.0 |
|
||||
| `crypto-common` | 0.1.7 | MIT OR Apache-2.0 |
|
||||
| `ctr` | 0.9.2 | MIT OR Apache-2.0 |
|
||||
| `dav-server` | 0.11.0 | Apache-2.0 |
|
||||
| `derive-where` | 1.6.1 | MIT OR Apache-2.0 |
|
||||
| `digest` | 0.10.7 | MIT OR Apache-2.0 |
|
||||
| `displaydoc` | 0.2.7 | MIT OR Apache-2.0 |
|
||||
| `dyn-clone` | 1.0.20 | MIT OR Apache-2.0 |
|
||||
| `equivalent` | 1.0.2 | Apache-2.0 OR MIT |
|
||||
| `errno` | 0.3.14 | MIT OR Apache-2.0 |
|
||||
| `fallible-iterator` | 0.3.0 | MIT/Apache-2.0 |
|
||||
| `fallible-streaming-iterator` | 0.1.9 | MIT/Apache-2.0 |
|
||||
| `find-msvc-tools` | 0.1.12 | MIT OR Apache-2.0 |
|
||||
| `fnv` | 1.0.7 | Apache-2.0 / MIT |
|
||||
| `foreign-types` | 0.5.0 | MIT/Apache-2.0 |
|
||||
| `foreign-types-macros` | 0.2.4 | MIT OR Apache-2.0 |
|
||||
| `foreign-types-shared` | 0.3.1 | MIT OR Apache-2.0 |
|
||||
| `form_urlencoded` | 1.2.2 | MIT OR Apache-2.0 |
|
||||
| `futures-channel` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `futures-core` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `futures-macro` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `futures-sink` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `futures-task` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `futures-util` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `generic-array` | 0.14.7 | MIT |
|
||||
| `getrandom` | 0.4.3 | MIT OR Apache-2.0 |
|
||||
| `getrandom` | 0.2.17 | MIT OR Apache-2.0 |
|
||||
| `ghash` | 0.5.1 | Apache-2.0 OR MIT |
|
||||
| `h2` | 0.4.19 | MIT |
|
||||
| `hashbrown` | 0.17.1 | MIT OR Apache-2.0 |
|
||||
| `hashbrown` | 0.14.5 | MIT OR Apache-2.0 |
|
||||
| `hashlink` | 0.9.1 | MIT OR Apache-2.0 |
|
||||
| `headers` | 0.4.1 | MIT |
|
||||
| `headers-core` | 0.3.0 | MIT |
|
||||
| `heck` | 0.5.0 | MIT OR Apache-2.0 |
|
||||
| `hex` | 0.4.3 | MIT OR Apache-2.0 |
|
||||
| `hex-conservative` | 0.2.3 | CC0-1.0 |
|
||||
| `htmlescape` | 0.3.1 | Apache-2.0 / MIT / MPL-2.0 |
|
||||
| `http` | 1.5.0 | MIT OR Apache-2.0 |
|
||||
| `httparse` | 1.10.1 | MIT OR Apache-2.0 |
|
||||
| `http-body` | 1.1.0 | MIT |
|
||||
| `http-body-util` | 0.1.5 | MIT |
|
||||
| `httpdate` | 1.0.3 | MIT OR Apache-2.0 |
|
||||
| `hyper` | 1.11.1 | MIT |
|
||||
| `hyper-util` | 0.1.20 | MIT |
|
||||
| `iana-time-zone` | 0.1.65 | MIT OR Apache-2.0 |
|
||||
| `iana-time-zone-haiku` | 0.1.2 | MIT OR Apache-2.0 |
|
||||
| `icu_collections` | 2.3.0 | Unicode-3.0 |
|
||||
| `icu_locale_core` | 2.3.0 | Unicode-3.0 |
|
||||
| `icu_normalizer` | 2.3.0 | Unicode-3.0 |
|
||||
| `icu_normalizer_data` | 2.3.0 | Unicode-3.0 |
|
||||
| `icu_properties` | 2.3.0 | Unicode-3.0 |
|
||||
| `icu_properties_data` | 2.3.0 | Unicode-3.0 |
|
||||
| `icu_provider` | 2.3.1 | Unicode-3.0 |
|
||||
| `idna` | 1.1.0 | MIT OR Apache-2.0 |
|
||||
| `idna_adapter` | 1.2.2 | Apache-2.0 OR MIT |
|
||||
| `indexmap` | 2.14.2 | Apache-2.0 OR MIT |
|
||||
| `inout` | 0.1.4 | MIT OR Apache-2.0 |
|
||||
| `is_terminal_polyfill` | 1.70.2 | MIT OR Apache-2.0 |
|
||||
| `itoa` | 1.0.18 | MIT OR Apache-2.0 |
|
||||
| `js-sys` | 0.3.105 | MIT OR Apache-2.0 |
|
||||
| `lazy_static` | 1.5.0 | MIT OR Apache-2.0 |
|
||||
| `libc` | 0.2.189 | MIT OR Apache-2.0 |
|
||||
| `libsqlite3-sys` | 0.30.1 | MIT |
|
||||
| `litemap` | 0.8.3 | Unicode-3.0 |
|
||||
| `lock_api` | 0.4.14 | MIT OR Apache-2.0 |
|
||||
| `log` | 0.4.34 | MIT OR Apache-2.0 |
|
||||
| `lz4_flex` | 0.11.6 | MIT |
|
||||
| `malloc_buf` | 0.0.6 | MIT |
|
||||
| `matchers` | 0.2.0 | MIT |
|
||||
| `memchr` | 2.8.3 | Unlicense OR MIT |
|
||||
| `mime` | 0.3.17 | MIT OR Apache-2.0 |
|
||||
| `mime_guess` | 2.0.5 | MIT |
|
||||
| `mio` | 1.2.3 | MIT |
|
||||
| `nu-ansi-term` | 0.50.3 | MIT |
|
||||
| `num-traits` | 0.2.19 | MIT OR Apache-2.0 |
|
||||
| `objc` | 0.2.7 | MIT OR Apache-2.0 |
|
||||
| `objc_id` | 0.1.1 | MIT |
|
||||
| `objc-foundation` | 0.1.1 | MIT OR Apache-2.0 |
|
||||
| `once_cell` | 1.21.4 | MIT OR Apache-2.0 |
|
||||
| `once_cell_polyfill` | 1.70.2 | MIT OR Apache-2.0 |
|
||||
| `opaque-debug` | 0.3.1 | MIT OR Apache-2.0 |
|
||||
| `padlock` | 0.2.0 | MIT OR Apache-2.0 |
|
||||
| `parking_lot` | 0.12.5 | MIT OR Apache-2.0 |
|
||||
| `parking_lot_core` | 0.9.12 | MIT OR Apache-2.0 |
|
||||
| `password-hash` | 0.5.0 | MIT OR Apache-2.0 |
|
||||
| `percent-encoding` | 2.3.2 | MIT OR Apache-2.0 |
|
||||
| `pin-project-lite` | 0.2.17 | Apache-2.0 OR MIT |
|
||||
| `pkg-config` | 0.3.34 | MIT OR Apache-2.0 |
|
||||
| `polyval` | 0.6.2 | Apache-2.0 OR MIT |
|
||||
| `potential_utf` | 0.1.6 | Unicode-3.0 |
|
||||
| `ppv-lite86` | 0.2.21 | MIT OR Apache-2.0 |
|
||||
| `proc-macro2` | 1.0.107 | MIT OR Apache-2.0 |
|
||||
| `quote` | 1.0.47 | MIT OR Apache-2.0 |
|
||||
| `rand` | 0.8.8 | MIT OR Apache-2.0 |
|
||||
| `rand_chacha` | 0.3.1 | MIT OR Apache-2.0 |
|
||||
| `rand_core` | 0.6.4 | MIT OR Apache-2.0 |
|
||||
| `redox_syscall` | 0.5.18 | MIT OR Apache-2.0 |
|
||||
| `r-efi` | 6.0.0 | MIT OR Apache-2.0 |
|
||||
| `regex-automata` | 0.4.18 | MIT OR Apache-2.0 |
|
||||
| `regex-syntax` | 0.8.11 | MIT OR Apache-2.0 |
|
||||
| `rpassword` | 7.5.4 | Apache-2.0 |
|
||||
| `rtoolbox` | 0.0.6 | Apache-2.0 |
|
||||
| `rusqlite` | 0.32.1 | MIT |
|
||||
| `rustversion` | 1.0.23 | MIT OR Apache-2.0 |
|
||||
| `scopeguard` | 1.2.0 | MIT OR Apache-2.0 |
|
||||
| `serde` | 1.0.229 | MIT OR Apache-2.0 |
|
||||
| `serde_core` | 1.0.229 | MIT OR Apache-2.0 |
|
||||
| `serde_derive` | 1.0.229 | MIT OR Apache-2.0 |
|
||||
| `serde_json` | 1.0.151 | MIT OR Apache-2.0 |
|
||||
| `sha1` | 0.10.7 | MIT OR Apache-2.0 |
|
||||
| `sharded-slab` | 0.1.7 | MIT |
|
||||
| `shlex` | 2.0.1 | MIT OR Apache-2.0 |
|
||||
| `signal-hook-registry` | 1.4.8 | MIT OR Apache-2.0 |
|
||||
| `slab` | 0.4.12 | MIT |
|
||||
| `smallvec` | 1.16.0 | MIT OR Apache-2.0 |
|
||||
| `socket2` | 0.6.5 | MIT OR Apache-2.0 |
|
||||
| `stable_deref_trait` | 1.2.1 | MIT OR Apache-2.0 |
|
||||
| `strsim` | 0.11.1 | MIT |
|
||||
| `subtle` | 2.6.1 | BSD-3-Clause |
|
||||
| `syn` | 3.0.5 | MIT OR Apache-2.0 |
|
||||
| `syn` | 2.0.119 | MIT OR Apache-2.0 |
|
||||
| `synstructure` | 0.13.2 | MIT |
|
||||
| `thiserror` | 2.0.20 | MIT OR Apache-2.0 |
|
||||
| `thiserror-impl` | 2.0.20 | MIT OR Apache-2.0 |
|
||||
| `thread_local` | 1.1.10 | MIT OR Apache-2.0 |
|
||||
| `tinystr` | 0.8.4 | Unicode-3.0 |
|
||||
| `tinyvec` | 1.13.2 | Zlib OR Apache-2.0 OR MIT |
|
||||
| `tinyvec_macros` | 0.1.1 | MIT OR Apache-2.0 OR Zlib |
|
||||
| `tokio` | 1.53.1 | MIT |
|
||||
| `tokio-macros` | 2.7.2 | MIT |
|
||||
| `tokio-util` | 0.7.19 | MIT |
|
||||
| `tracing` | 0.1.44 | MIT |
|
||||
| `tracing-attributes` | 0.1.31 | MIT |
|
||||
| `tracing-core` | 0.1.36 | MIT |
|
||||
| `tracing-log` | 0.2.0 | MIT |
|
||||
| `tracing-subscriber` | 0.3.23 | MIT |
|
||||
| `tray-item` | 0.10.0 | MIT |
|
||||
| `twox-hash` | 2.1.4 | MIT |
|
||||
| `typenum` | 1.20.1 | MIT OR Apache-2.0 |
|
||||
| `unicase` | 2.9.0 | MIT OR Apache-2.0 |
|
||||
| `unicode-ident` | 1.0.24 | (MIT OR Apache-2.0) AND Unicode-3.0 |
|
||||
| `unicode-normalization` | 0.1.25 | MIT OR Apache-2.0 |
|
||||
| `universal-hash` | 0.5.1 | MIT OR Apache-2.0 |
|
||||
| `url` | 2.5.8 | MIT OR Apache-2.0 |
|
||||
| `utf8_iter` | 1.0.4 | Apache-2.0 OR MIT |
|
||||
| `utf8parse` | 0.2.2 | Apache-2.0 OR MIT |
|
||||
| `uuid` | 1.26.0 | Apache-2.0 OR MIT |
|
||||
| `valuable` | 0.1.1 | MIT OR Apache-2.0 |
|
||||
| `vcpkg` | 0.2.15 | MIT/Apache-2.0 |
|
||||
| `version_check` | 0.9.5 | MIT/Apache-2.0 |
|
||||
| `wasi` | 0.11.1+wasi-snapshot-preview1 | MIT OR Apache-2.0 |
|
||||
| `wasm-bindgen` | 0.2.128 | MIT OR Apache-2.0 |
|
||||
| `wasm-bindgen-macro` | 0.2.128 | MIT OR Apache-2.0 |
|
||||
| `wasm-bindgen-macro-support` | 0.2.128 | MIT OR Apache-2.0 |
|
||||
| `wasm-bindgen-shared` | 0.2.128 | MIT OR Apache-2.0 |
|
||||
| `windows_aarch64_gnullvm` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_aarch64_msvc` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_i686_gnu` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_i686_gnullvm` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_i686_msvc` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_x86_64_gnu` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_x86_64_gnullvm` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows_x86_64_msvc` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `windows-core` | 0.62.2 | MIT OR Apache-2.0 |
|
||||
| `windows-implement` | 0.60.2 | MIT OR Apache-2.0 |
|
||||
| `windows-interface` | 0.59.3 | MIT OR Apache-2.0 |
|
||||
| `windows-link` | 0.2.1 | MIT OR Apache-2.0 |
|
||||
| `windows-result` | 0.4.1 | MIT OR Apache-2.0 |
|
||||
| `windows-strings` | 0.5.1 | MIT OR Apache-2.0 |
|
||||
| `windows-sys` | 0.61.2 | MIT OR Apache-2.0 |
|
||||
| `windows-sys` | 0.52.0 | MIT OR Apache-2.0 |
|
||||
| `windows-targets` | 0.52.6 | MIT OR Apache-2.0 |
|
||||
| `writeable` | 0.6.4 | Unicode-3.0 |
|
||||
| `xml` | 1.4.0 | MIT |
|
||||
| `xml-rs` | 1.0.0 | MIT |
|
||||
| `xmltree` | 0.12.0 | MIT |
|
||||
| `yoke` | 0.8.3 | Unicode-3.0 |
|
||||
| `yoke-derive` | 0.8.2 | Unicode-3.0 |
|
||||
| `zerocopy` | 0.8.56 | BSD-2-Clause OR Apache-2.0 OR MIT |
|
||||
| `zerocopy-derive` | 0.8.56 | MIT OR Apache-2.0 |
|
||||
| `zerofrom` | 0.1.8 | Unicode-3.0 |
|
||||
| `zerofrom-derive` | 0.1.7 | Unicode-3.0 |
|
||||
| `zeroize` | 1.9.0 | Apache-2.0 OR MIT |
|
||||
| `zeroize_derive` | 1.5.0 | Apache-2.0 OR MIT |
|
||||
| `zerotrie` | 0.2.5 | Unicode-3.0 |
|
||||
| `zerovec` | 0.11.8 | Unicode-3.0 |
|
||||
| `zerovec-derive` | 0.11.6 | Unicode-3.0 |
|
||||
| `zmij` | 1.0.23 | MIT |
|
||||
|
||||
---
|
||||
|
||||
## 2. License Texts
|
||||
|
||||
### The MIT License
|
||||
```text
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
### Apache License, Version 2.0
|
||||
```text
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work.
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file.
|
||||
```
|
||||
|
||||
### 3-Clause BSD License
|
||||
```text
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
### SQLite Copyright & Blessing (Public Domain)
|
||||
```text
|
||||
The author disclaims copyright to this source code. In place of a legal notice,
|
||||
here is a blessing:
|
||||
|
||||
May you do good and not evil.
|
||||
May you find forgiveness for yourself and forgive others.
|
||||
May you share freely, never taking more than you give.
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 546 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 830 KiB |
@@ -0,0 +1,28 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=assets/icon.ico");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if target_os == "windows" {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
let rc_path = format!("{}/sanctum.rc", out_dir);
|
||||
let res_path = format!("{}/sanctum.res", out_dir);
|
||||
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let ico_path = format!("{}/assets/icon.ico", manifest_dir).replace('\\', "/");
|
||||
|
||||
if std::path::Path::new(&ico_path).exists() {
|
||||
let rc_content = format!("1 ICON \"{}\"\n", ico_path);
|
||||
if std::fs::write(&rc_path, rc_content).is_ok() {
|
||||
let status = std::process::Command::new("windres")
|
||||
.args(["-i", &rc_path, "-O", "coff", "-o", &res_path])
|
||||
.status();
|
||||
if let Ok(s) = status {
|
||||
if s.success() {
|
||||
println!("cargo:rustc-link-arg={}", res_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": "0.4.1",
|
||||
"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.1/sanctum-v0.4.1-windows-x86_64.zip",
|
||||
"hash": "dc0980312857505a078a3693d81a5d16853c3a94152f9104004dc6c4fd929bf7",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.1
|
||||
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.1
|
||||
Installers:
|
||||
- Architecture: x64
|
||||
InstallerType: zip
|
||||
NestedInstallerType: portable
|
||||
NestedInstallerFiles:
|
||||
- RelativeFilePath: sanctum.exe
|
||||
PortableCommandAlias: sanctum
|
||||
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.4.1/sanctum-v0.4.1-windows-x86_64.zip
|
||||
InstallerSha256: dc0980312857505a078a3693d81a5d16853c3a94152f9104004dc6c4fd929bf7
|
||||
ManifestType: singleton
|
||||
ManifestVersion: 1.6.0
|
||||
@@ -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."
|
||||
@@ -55,16 +55,24 @@ $PackageName = "sanctum-v$Version-windows-x86_64"
|
||||
$StagingDir = Join-Path $DistDir $PackageName
|
||||
$ZipFile = Join-Path $DistDir "$PackageName.zip"
|
||||
|
||||
if (Test-Path $DistDir) {
|
||||
Remove-Item $DistDir -Recurse -Force
|
||||
if (-not (Test-Path $DistDir)) {
|
||||
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
|
||||
|
||||
$ExeSource = Join-Path $ProjectRoot "target\release\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 "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 "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 "CHANGELOG.md") (Join-Path $StagingDir "CHANGELOG.md")
|
||||
Copy-Item (Join-Path $ProjectRoot "assets") (Join-Path $StagingDir "assets") -Recurse
|
||||
|
||||
# ZIP-Archiv schnueren
|
||||
Compress-Archive -Path "$StagingDir\*" -DestinationPath $ZipFile -Force
|
||||
@@ -79,7 +87,17 @@ $ZipHash = (Get-FileHash -Path $ZipFile -Algorithm SHA256).Hash.ToLower()
|
||||
$ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower()
|
||||
|
||||
$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
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
param (
|
||||
[string]$Token,
|
||||
[string]$GiteaUrl = "https://gitea.pansi.eu",
|
||||
[string]$Owner = "harald",
|
||||
[string]$Repo = "sanctum"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectRoot = Split-Path -Parent $ScriptDir
|
||||
$CargoToml = Get-Content (Join-Path $ProjectRoot "Cargo.toml") -Raw
|
||||
if ($CargoToml -match 'version\s*=\s*"([^"]+)"') {
|
||||
$Version = $matches[1]
|
||||
} else {
|
||||
Write-Error "Version nicht gefunden."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$TagName = "v$Version"
|
||||
$DistDir = Join-Path $ProjectRoot "dist"
|
||||
$ZipFile = Join-Path $DistDir "sanctum-$TagName-windows-x86_64.zip"
|
||||
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
|
||||
|
||||
if (-not (Test-Path $ZipFile)) {
|
||||
Write-Host "Dist-Paket existiert noch nicht. Baue Paket..." -ForegroundColor Yellow
|
||||
& (Join-Path $ScriptDir "package-release.ps1")
|
||||
}
|
||||
|
||||
# .env-Datei laden falls vorhanden (ist in .gitignore)
|
||||
$EnvFile = Join-Path $ProjectRoot ".env"
|
||||
if (Test-Path $EnvFile) {
|
||||
Get-Content $EnvFile | ForEach-Object {
|
||||
if ($_ -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$') {
|
||||
$val = $matches[2].Trim('"', "'")
|
||||
[System.Environment]::SetEnvironmentVariable($matches[1], $val, "Process")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Token ermitteln (Parameter -> Environment -> Prompt)
|
||||
if (-not $Token) {
|
||||
if ($env:GITEA_TOKEN) {
|
||||
$Token = $env:GITEA_TOKEN
|
||||
} else {
|
||||
$SecureToken = Read-Host "Gitea Personal Access Token (mit 'repo'-Scope)" -AsSecureString
|
||||
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureToken)
|
||||
$Token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Token -or $Token.Trim().Length -eq 0) {
|
||||
Write-Error "Kein Token angegeben. Abgebrochen."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Write-Host " Veroeffentliche Release $TagName auf Gitea" -ForegroundColor Cyan
|
||||
Write-Host " Repository: $Owner/$Repo ($GiteaUrl)" -ForegroundColor Cyan
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
|
||||
$Headers = @{
|
||||
"Authorization" = "token $Token"
|
||||
"Accept" = "application/json"
|
||||
}
|
||||
|
||||
# Pruefen ob Release bereits existiert
|
||||
$ExistingReleases = try {
|
||||
Invoke-RestMethod -Uri "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases" -Headers $Headers -Method Get
|
||||
} catch {
|
||||
@()
|
||||
}
|
||||
|
||||
$Release = $ExistingReleases | Where-Object { $_.tag_name -eq $TagName }
|
||||
|
||||
if (-not $Release) {
|
||||
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
$Changelog = [System.IO.File]::ReadAllText((Join-Path $ProjectRoot "CHANGELOG.md"), $Utf8NoBom)
|
||||
$PayloadPath = Join-Path $DistDir "release_req.json"
|
||||
$Payload = @{
|
||||
tag_name = $TagName
|
||||
name = "Sanctum $TagName (Windows x86_64)"
|
||||
body = $Changelog
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
} | ConvertTo-Json -Depth 5
|
||||
[System.IO.File]::WriteAllText($PayloadPath, $Payload, $Utf8NoBom)
|
||||
|
||||
|
||||
$CreateResp = & curl.exe -s -X POST "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases" `
|
||||
-H "Authorization: token $Token" `
|
||||
-H "Content-Type: application/json; charset=utf-8" `
|
||||
--data-binary "@$PayloadPath"
|
||||
|
||||
Remove-Item $PayloadPath -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$Release = $CreateResp | ConvertFrom-Json
|
||||
if (-not $Release.id) {
|
||||
Write-Error "Fehler beim Erstellen des Releases: $CreateResp"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[OK] Release $TagName auf Gitea angelegt (ID: $($Release.id))." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[INFO] Release $TagName existiert bereits (ID: $($Release.id))." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
|
||||
$UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets"
|
||||
$AuthHeader = "Authorization: token $Token"
|
||||
|
||||
# Hilfsfunktion zum sauberen Hochladen/Ersetzen von Assets
|
||||
function Upload-ReleaseAsset {
|
||||
param (
|
||||
[string]$FilePath,
|
||||
[string]$AssetName
|
||||
)
|
||||
|
||||
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
|
||||
Upload-ReleaseAsset -FilePath $ChecksumFile -AssetName "SHA256SUMS.txt"
|
||||
|
||||
|
||||
Write-Host "`n============================================================" -ForegroundColor Green
|
||||
Write-Host " Release $TagName erfolgreich auf Gitea veroeffentlicht!" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " URL: $GiteaUrl/$Owner/$Repo/releases/tag/$TagName`n"
|
||||
+1046
File diff suppressed because it is too large
Load Diff
+659
-25
@@ -10,9 +10,15 @@ use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub const MAGIC_BYTES: &[u8; 8] = b"SANCTUM\0";
|
||||
pub const FORMAT_VERSION: u32 = 1;
|
||||
pub const FORMAT_VERSION_V1: u32 = 1;
|
||||
pub const FORMAT_VERSION_V2: u32 = 2;
|
||||
pub const FORMAT_VERSION: u32 = FORMAT_VERSION_V2;
|
||||
pub const CHUNK_SIZE: usize = 1024 * 1024; // 1 MB
|
||||
|
||||
/// Kompressions-Flags für Chunk-Payloads in Formatversion >= 2
|
||||
pub const COMPRESSION_NONE: u8 = 0x00;
|
||||
pub const COMPRESSION_LZ4: u8 = 0x01;
|
||||
|
||||
pub const DEFAULT_MEMORY_COST_KIB: u32 = 64 * 1024; // 64 MB
|
||||
pub const DEFAULT_TIME_COST: u32 = 3;
|
||||
pub const DEFAULT_PARALLELISM: u32 = 4;
|
||||
@@ -72,11 +78,10 @@ pub fn generate_salt() -> [u8; 16] {
|
||||
salt
|
||||
}
|
||||
|
||||
/// Verschlüsselt den DEK mit dem KEK via AES-256-GCM.
|
||||
/// Gibt (wrapped_dek_32_bytes, nonce_12_bytes, tag_16_bytes) zurück.
|
||||
pub fn wrap_dek(
|
||||
/// Verschlüsselt beliebige Schlüsseldaten (32B DEK, 40B Slot0-Payload oder 72B Slot1-Payload) via AES-256-GCM.
|
||||
pub fn wrap_key_payload(
|
||||
kek: &[u8; 32],
|
||||
dek: &[u8; 32],
|
||||
payload: &[u8],
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let cipher = Aes256Gcm::new_from_slice(kek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
@@ -85,10 +90,10 @@ pub fn wrap_dek(
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let mut buffer = dek.to_vec();
|
||||
let mut buffer = payload.to_vec();
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer)
|
||||
.map_err(|e| anyhow::anyhow!("DEK-Wrapping fehlgeschlagen: {e}"))?;
|
||||
.map_err(|e| anyhow::anyhow!("Key-Wrapping fehlgeschlagen: {e}"))?;
|
||||
|
||||
let mut tag_bytes = [0u8; 16];
|
||||
tag_bytes.copy_from_slice(tag.as_slice());
|
||||
@@ -96,6 +101,36 @@ pub fn wrap_dek(
|
||||
Ok((buffer, nonce_bytes, tag_bytes))
|
||||
}
|
||||
|
||||
/// Entschlüsselt beliebige Schlüsseldaten via AES-256-GCM und validiert die Authentizität.
|
||||
pub fn unwrap_key_payload(
|
||||
kek: &[u8; 32],
|
||||
wrapped_payload: &[u8],
|
||||
nonce_bytes: &[u8; 12],
|
||||
tag_bytes: &[u8; 16],
|
||||
) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let cipher = Aes256Gcm::new_from_slice(kek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
let tag = Tag::from_slice(tag_bytes);
|
||||
|
||||
let mut buffer = wrapped_payload.to_vec();
|
||||
cipher
|
||||
.decrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer, tag)
|
||||
.map_err(|_| anyhow::anyhow!("Passwort falsch oder Header beschädigt (AEAD Authentifizierungsfehler)"))?;
|
||||
|
||||
Ok(Zeroizing::new(buffer))
|
||||
}
|
||||
|
||||
/// Verschlüsselt den DEK (32 Bytes) mit dem KEK via AES-256-GCM.
|
||||
/// Gibt (wrapped_dek_32_bytes, nonce_12_bytes, tag_16_bytes) zurück.
|
||||
pub fn wrap_dek(
|
||||
kek: &[u8; 32],
|
||||
dek: &[u8; 32],
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
wrap_key_payload(kek, dek)
|
||||
}
|
||||
|
||||
/// Entschlüsselt den DEK mit dem KEK via AES-256-GCM und validiert die Authentizität.
|
||||
pub fn unwrap_dek(
|
||||
kek: &[u8; 32],
|
||||
@@ -103,23 +138,314 @@ pub fn unwrap_dek(
|
||||
nonce_bytes: &[u8; 12],
|
||||
tag_bytes: &[u8; 16],
|
||||
) -> Result<Zeroizing<[u8; 32]>> {
|
||||
if wrapped_dek.len() != 32 {
|
||||
bail!("Ungültige wrapped_dek Länge: erwartet 32 Bytes, erhalten {}", wrapped_dek.len());
|
||||
let payload = unwrap_key_payload(kek, wrapped_dek, nonce_bytes, tag_bytes)?;
|
||||
if payload.len() < 32 {
|
||||
bail!("Ungültige wrapped_dek Länge: erwartet mindestens 32 Bytes, erhalten {}", payload.len());
|
||||
}
|
||||
|
||||
let cipher = Aes256Gcm::new_from_slice(kek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
let mut dek = Zeroizing::new([0u8; 32]);
|
||||
dek.copy_from_slice(&payload[0..32]);
|
||||
Ok(dek)
|
||||
}
|
||||
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
let tag = Tag::from_slice(tag_bytes);
|
||||
/// Verschlüsselt den Slot-0 Payload (32 Bytes DEK_0 || 8 Bytes carrier_node_id Little-Endian).
|
||||
pub fn wrap_slot0_payload(
|
||||
kek: &[u8; 32],
|
||||
dek_0: &[u8; 32],
|
||||
carrier_node_id: i64,
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let mut payload = Vec::with_capacity(40);
|
||||
payload.extend_from_slice(dek_0);
|
||||
payload.extend_from_slice(&carrier_node_id.to_le_bytes());
|
||||
wrap_key_payload(kek, &payload)
|
||||
}
|
||||
|
||||
let mut buffer = wrapped_dek.to_vec();
|
||||
cipher
|
||||
.decrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer, tag)
|
||||
.map_err(|_| anyhow::anyhow!("Passwort falsch oder Header beschädigt (AEAD Authentifizierungsfehler)"))?;
|
||||
/// Verschlüsselt den Slot-1 Payload für Modell A (32 Bytes DEK_1 || 32 Bytes DEK_0 || 8 Bytes carrier_node_id Little-Endian).
|
||||
pub fn wrap_slot1_payload(
|
||||
kek: &[u8; 32],
|
||||
dek_1: &[u8; 32],
|
||||
dek_0: &[u8; 32],
|
||||
carrier_node_id: i64,
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let mut payload = Vec::with_capacity(72);
|
||||
payload.extend_from_slice(dek_1);
|
||||
payload.extend_from_slice(dek_0);
|
||||
payload.extend_from_slice(&carrier_node_id.to_le_bytes());
|
||||
wrap_key_payload(kek, &payload)
|
||||
}
|
||||
|
||||
/// Erzeugt einen Dummy-Header-Slot mit kryptografisch sicherem Zufallsrauschen derselben Länge wie
|
||||
/// ein echter Modell-A Slot 1 (72 Bytes wrapped Payload). Dadurch sind Standard-Container von
|
||||
/// Containern mit Hidden Vault auf Bitebene und Entropieebene ununterscheidbar (Plausible Deniability).
|
||||
pub fn generate_dummy_slot() -> (Vec<u8>, [u8; 12], [u8; 16], [u8; 16]) {
|
||||
let mut wrapped_dek = vec![0u8; 72];
|
||||
let mut nonce = [0u8; 12];
|
||||
let mut tag = [0u8; 16];
|
||||
let mut salt = [0u8; 16];
|
||||
OsRng.fill_bytes(&mut wrapped_dek);
|
||||
OsRng.fill_bytes(&mut nonce);
|
||||
OsRng.fill_bytes(&mut tag);
|
||||
OsRng.fill_bytes(&mut salt);
|
||||
(wrapped_dek, nonce, tag, salt)
|
||||
}
|
||||
|
||||
/// Erzeugt die 16-Byte Associated Data (AAD) für einen Dateinamen im Hidden Vault,
|
||||
/// um Directory-Hijacking und Cross-Node Name-Substitution-Angriffe kryptografisch zu verhindern:
|
||||
/// Magic "SANCNAME" (8 Bytes) || parent_id (8 Bytes Little-Endian).
|
||||
#[inline]
|
||||
pub fn build_name_aad(parent_id: i64) -> [u8; 16] {
|
||||
let mut aad = [0u8; 16];
|
||||
aad[..8].copy_from_slice(b"SANCNAME");
|
||||
aad[8..].copy_from_slice(&parent_id.to_le_bytes());
|
||||
aad
|
||||
}
|
||||
|
||||
/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM und bindet die parent_id als AAD ein.
|
||||
/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind
|
||||
/// und verhindert, dass verschlüsselte Knoten zwischen Ordnern verschoben oder vertauscht werden können.
|
||||
/// Verwendet reines Hex-Encoding ohne verräterisches Präfix (12B Nonce + 16B Tag + Ciphertext).
|
||||
pub fn encrypt_node_name(dek: &[u8; 32], parent_id: i64, name: &str) -> String {
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let cipher = Aes256Gcm::new_from_slice(dek).expect("AES init");
|
||||
let mut buffer = name.as_bytes().to_vec();
|
||||
let aad = build_name_aad(parent_id);
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), &aad, &mut buffer)
|
||||
.expect("Name encryption");
|
||||
let mut combined = Vec::with_capacity(12 + 16 + buffer.len());
|
||||
combined.extend_from_slice(&nonce_bytes);
|
||||
combined.extend_from_slice(tag.as_slice());
|
||||
combined.extend_from_slice(&buffer);
|
||||
hex::encode(combined)
|
||||
}
|
||||
|
||||
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
|
||||
/// Prüft primär die kryptografische Bindung an parent_id; bietet transparenten Fallback
|
||||
/// auf die statische AAD für ältere Container (Abwärtskompatibilität).
|
||||
pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option<String> {
|
||||
// Abwärtskompatibilität für alte v0.2.0 $h$<nonce>$<tag>$<ct> Namen
|
||||
if let Some(rest) = stored.strip_prefix("$h$") {
|
||||
let parts: Vec<&str> = rest.split('$').collect();
|
||||
if parts.len() == 3 {
|
||||
if let (Ok(nonce_bytes), Ok(tag_bytes), Ok(ct_bytes)) = (
|
||||
hex::decode(parts[0]),
|
||||
hex::decode(parts[1]),
|
||||
hex::decode(parts[2]),
|
||||
) {
|
||||
if nonce_bytes.len() == 12 && tag_bytes.len() == 16 {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek).ok()?;
|
||||
// 1. Primär: Authentifizierung mit parent_id AAD
|
||||
let aad = build_name_aad(parent_id);
|
||||
let mut buffer = ct_bytes.clone();
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(&nonce_bytes),
|
||||
&aad,
|
||||
&mut buffer,
|
||||
Tag::from_slice(&tag_bytes),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer).ok();
|
||||
}
|
||||
|
||||
// 2. Fallback: Statische AAD für echte Legacy-Dateinamen
|
||||
let mut buffer_legacy = ct_bytes;
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(&nonce_bytes),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer_legacy,
|
||||
Tag::from_slice(&tag_bytes),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer_legacy).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Reiner Hex-String (12B Nonce + 16B Tag + Ciphertext)
|
||||
if stored.len() >= 56 {
|
||||
if let Ok(bytes) = hex::decode(stored) {
|
||||
if bytes.len() >= 28 {
|
||||
let nonce = &bytes[0..12];
|
||||
let tag = &bytes[12..28];
|
||||
let ct = &bytes[28..];
|
||||
|
||||
if let Ok(cipher) = Aes256Gcm::new_from_slice(dek) {
|
||||
// 1. Primär: Authentifizierung mit parent_id AAD
|
||||
let aad = build_name_aad(parent_id);
|
||||
let mut buffer = ct.to_vec();
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(nonce),
|
||||
&aad,
|
||||
&mut buffer,
|
||||
Tag::from_slice(tag),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer).ok();
|
||||
}
|
||||
|
||||
// 2. Fallback: Alte statische AAD für bestehende Container
|
||||
let mut buffer_legacy = ct.to_vec();
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(nonce),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer_legacy,
|
||||
Tag::from_slice(tag),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer_legacy).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Kodiert den 32-Byte (256-Bit) DEK in eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase (englisch) mit 8-Bit Checksumme.
|
||||
pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
|
||||
let mnemonic = bip39::Mnemonic::from_entropy(dek)
|
||||
.map_err(|e| anyhow::anyhow!("Fehler beim Erzeugen der BIP-39 Notfallphrase: {e}"))?;
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
|
||||
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
|
||||
let a_chars: Vec<char> = a.chars().collect();
|
||||
let b_chars: Vec<char> = b.chars().collect();
|
||||
let (m, n) = (a_chars.len(), b_chars.len());
|
||||
|
||||
let mut dp = vec![vec![0usize; n + 1]; m + 1];
|
||||
for i in 0..=m {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for j in 0..=n {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
|
||||
for i in 1..=m {
|
||||
for j in 1..=n {
|
||||
let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
|
||||
dp[i][j] = (dp[i - 1][j] + 1)
|
||||
.min(dp[i][j - 1] + 1)
|
||||
.min(dp[i - 1][j - 1] + cost);
|
||||
}
|
||||
}
|
||||
|
||||
dp[m][n]
|
||||
}
|
||||
|
||||
/// Sucht den besten Korrektur-Kandidaten aus der BIP-39 Wortliste für ein fehlerhaftes Wort.
|
||||
pub fn suggest_bip39_word(word: &str) -> Option<&'static str> {
|
||||
let word_list = bip39::Language::English.word_list();
|
||||
let mut best_match = None;
|
||||
let mut min_dist = usize::MAX;
|
||||
|
||||
for &valid in word_list {
|
||||
let dist = levenshtein_distance(word, valid);
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
best_match = Some(valid);
|
||||
}
|
||||
}
|
||||
|
||||
// Vorschlag nur zurückgeben, wenn Distanz klein genug ist (z. B. <= 2)
|
||||
if min_dist <= 2 {
|
||||
best_match
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalisiert eine eingegebene BIP-39 Phrase:
|
||||
/// - Wandelt in Kleinbuchstaben um (to_lowercase)
|
||||
/// - Ersetzt Satzzeichen und Trennzeichen durch Leerzeichen
|
||||
/// - Bereinigt überflüssige Leerzeichen (Whitespace-Collapsing)
|
||||
pub fn normalize_mnemonic_phrase(phrase: &str) -> Vec<String> {
|
||||
let cleaned: String = phrase
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_alphabetic() {
|
||||
c.to_ascii_lowercase()
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
cleaned
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Dekodiert eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase zurück in den 32-Byte DEK.
|
||||
/// Validiert dabei Wörter und die integrierte BIP-39 Prüfsumme mit präziser Fehlerdiagnose.
|
||||
pub fn mnemonic_to_dek(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
|
||||
let words = normalize_mnemonic_phrase(phrase);
|
||||
|
||||
if words.is_empty() {
|
||||
bail!("Die Notfallphrase darf nicht leer sein.");
|
||||
}
|
||||
|
||||
if words.len() != 24 {
|
||||
bail!(
|
||||
"Ungültige Wortanzahl in der Notfallphrase: Erwartet werden genau 24 Wörter, eingegeben wurden jedoch {} Wörter.",
|
||||
words.len()
|
||||
);
|
||||
}
|
||||
|
||||
let word_list = bip39::Language::English.word_list();
|
||||
let mut invalid_words = Vec::new();
|
||||
|
||||
for (idx, word) in words.iter().enumerate() {
|
||||
if !word_list.contains(&word.as_str()) {
|
||||
let suggestion = suggest_bip39_word(word);
|
||||
let msg = if let Some(sug) = suggestion {
|
||||
format!("Wort #{} '{}' ist ungültig (Meinten Sie '{}'?)", idx + 1, word, sug)
|
||||
} else {
|
||||
format!("Wort #{} '{}' ist ungültig (nicht im BIP-39 Wörterbuch)", idx + 1, word)
|
||||
};
|
||||
invalid_words.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_words.is_empty() {
|
||||
bail!(
|
||||
"Ungültige Wörter in der Notfallphrase festgestellt:\n • {}",
|
||||
invalid_words.join("\n • ")
|
||||
);
|
||||
}
|
||||
|
||||
let normalized_phrase = words.join(" ");
|
||||
let mnemonic = bip39::Mnemonic::parse_normalized(&normalized_phrase)
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"Alle 24 Wörter sind gültige BIP-39 Wörter, aber die Prüfsumme (Checksumme) ist ungültig. \
|
||||
Bitte prüfen Sie die exakte Reihenfolge der Wörter oder das 24. Wort."
|
||||
)
|
||||
})?;
|
||||
|
||||
let entropy = mnemonic.to_entropy();
|
||||
if entropy.len() != 32 {
|
||||
bail!(
|
||||
"Ungültige Entropielänge aus Mnemonic: erwartet 32 Bytes (24 Wörter), erhalten {}",
|
||||
entropy.len()
|
||||
);
|
||||
}
|
||||
|
||||
let mut dek = Zeroizing::new([0u8; 32]);
|
||||
dek.copy_from_slice(&buffer);
|
||||
dek.copy_from_slice(&entropy);
|
||||
Ok(dek)
|
||||
}
|
||||
|
||||
@@ -134,12 +460,15 @@ pub fn build_chunk_aad(node_id: i64, chunk_index: u32) -> [u8; 16] {
|
||||
}
|
||||
|
||||
/// Verschlüsselt einen Payload-Chunk mit dem DEK via AES-256-GCM unter Einbindung von AAD.
|
||||
/// In Formatversion >= 2 wird der Chunk vor der Verschlüsselung transparent mit LZ4 komprimiert,
|
||||
/// sofern dadurch eine Größenreduktion erzielt wird.
|
||||
/// Gibt (ciphertext, nonce_12_bytes, tag_16_bytes) zurück.
|
||||
pub fn encrypt_chunk(
|
||||
dek: &[u8; 32],
|
||||
node_id: i64,
|
||||
chunk_index: u32,
|
||||
plaintext: &[u8],
|
||||
format_version: u32,
|
||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
@@ -150,7 +479,28 @@ pub fn encrypt_chunk(
|
||||
|
||||
let aad = build_chunk_aad(node_id, chunk_index);
|
||||
|
||||
let mut buffer = plaintext.to_vec();
|
||||
let mut buffer = if format_version >= FORMAT_VERSION_V2 {
|
||||
if plaintext.is_empty() {
|
||||
vec![COMPRESSION_NONE]
|
||||
} else {
|
||||
let compressed = lz4_flex::compress_prepend_size(plaintext);
|
||||
// Nur komprimieren, wenn mindestens 64 Bytes eingespart werden (+1 Byte für das Flag)
|
||||
if compressed.len() + 64 <= plaintext.len() {
|
||||
let mut buf = Vec::with_capacity(compressed.len() + 1);
|
||||
buf.push(COMPRESSION_LZ4);
|
||||
buf.extend_from_slice(&compressed);
|
||||
buf
|
||||
} else {
|
||||
let mut buf = Vec::with_capacity(plaintext.len() + 1);
|
||||
buf.push(COMPRESSION_NONE);
|
||||
buf.extend_from_slice(plaintext);
|
||||
buf
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plaintext.to_vec()
|
||||
};
|
||||
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(nonce, &aad, &mut buffer)
|
||||
.map_err(|e| anyhow::anyhow!("Chunk-Verschlüsselung fehlgeschlagen: {e}"))?;
|
||||
@@ -162,6 +512,7 @@ pub fn encrypt_chunk(
|
||||
}
|
||||
|
||||
/// Entschlüsselt und authentifiziert einen Payload-Chunk mit dem DEK via AES-256-GCM.
|
||||
/// Dekomprimiert LZ4-gepackte Chunks automatisch (in Formatversion >= 2).
|
||||
pub fn decrypt_chunk(
|
||||
dek: &[u8; 32],
|
||||
node_id: i64,
|
||||
@@ -169,6 +520,7 @@ pub fn decrypt_chunk(
|
||||
ciphertext: &[u8],
|
||||
nonce_bytes: &[u8; 12],
|
||||
tag_bytes: &[u8; 16],
|
||||
format_version: u32,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek)
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||
@@ -182,8 +534,36 @@ pub fn decrypt_chunk(
|
||||
.decrypt_in_place_detached(nonce, &aad, &mut buffer, tag)
|
||||
.map_err(|_| anyhow::anyhow!("Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"))?;
|
||||
|
||||
if format_version >= FORMAT_VERSION_V2 {
|
||||
if buffer.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
match buffer[0] {
|
||||
COMPRESSION_NONE => Ok(buffer[1..].to_vec()),
|
||||
COMPRESSION_LZ4 => {
|
||||
let payload = &buffer[1..];
|
||||
if payload.len() < 4 {
|
||||
bail!("LZ4-Chunk beschädigt: Payload zu kurz für Längen-Präfix");
|
||||
}
|
||||
let uncompressed_size = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
|
||||
if uncompressed_size > CHUNK_SIZE {
|
||||
bail!(
|
||||
"LZ4-Dekomprimierungsfehler: Decompression-Bomb Schutz ausgelöst (angeforderte Größe {} Bytes > Limit {} Bytes)",
|
||||
uncompressed_size,
|
||||
CHUNK_SIZE
|
||||
);
|
||||
}
|
||||
let decompressed = lz4_flex::decompress_size_prepended(payload)
|
||||
.map_err(|e| anyhow::anyhow!("LZ4-Dekomprimierungsfehler im Chunk: {e}"))?;
|
||||
Ok(decompressed)
|
||||
}
|
||||
other => bail!("Unbekannte Chunk-Kompressionsmethode: 0x{:02x}", other),
|
||||
}
|
||||
} else {
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -226,23 +606,277 @@ mod tests {
|
||||
let node_id = 42i64;
|
||||
let chunk_index = 0u32;
|
||||
|
||||
let (ciphertext, nonce, tag) = encrypt_chunk(&dek, node_id, chunk_index, plaintext).unwrap();
|
||||
let (ciphertext, nonce, tag) =
|
||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2).unwrap();
|
||||
|
||||
// Reguläre Entschlüsselung
|
||||
let decrypted = decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag).unwrap();
|
||||
// Reguläre Entschlüsselung (v2)
|
||||
let decrypted =
|
||||
decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// Swap Attack 1: Falsche node_id (Chunk in andere Datei verschoben)
|
||||
let swap_node_err = decrypt_chunk(&dek, 99i64, chunk_index, &ciphertext, &nonce, &tag);
|
||||
let swap_node_err =
|
||||
decrypt_chunk(&dek, 99i64, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2);
|
||||
assert!(swap_node_err.is_err());
|
||||
|
||||
// Swap Attack 2: Falscher chunk_index (Chunk innerhalb derselben Datei verschoben)
|
||||
let swap_idx_err = decrypt_chunk(&dek, node_id, 1u32, &ciphertext, &nonce, &tag);
|
||||
let swap_idx_err =
|
||||
decrypt_chunk(&dek, node_id, 1u32, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2);
|
||||
assert!(swap_idx_err.is_err());
|
||||
|
||||
// Manipulation des Ciphertexts
|
||||
let mut tampered_ct = ciphertext.clone();
|
||||
tampered_ct[0] ^= 0x01;
|
||||
assert!(decrypt_chunk(&dek, node_id, chunk_index, &tampered_ct, &nonce, &tag).is_err());
|
||||
assert!(
|
||||
decrypt_chunk(&dek, node_id, chunk_index, &tampered_ct, &nonce, &tag, FORMAT_VERSION_V2).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lz4_chunk_compression_efficiency() {
|
||||
let dek = generate_dek();
|
||||
// Stark komprimierbarer Text (z.B. Logdatei, JSON, Quellcode)
|
||||
let repeated_text = "Sanctum Secure Vault Storage System ".repeat(500);
|
||||
let plaintext = repeated_text.as_bytes();
|
||||
let node_id = 10i64;
|
||||
let chunk_index = 0u32;
|
||||
|
||||
let (ciphertext, nonce, tag) =
|
||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2).unwrap();
|
||||
|
||||
// Der komprimierte Ciphertext muss signifikant kleiner sein als der Klartext
|
||||
assert!(
|
||||
ciphertext.len() < plaintext.len() / 5,
|
||||
"Ciphertext ({}) sollte drastisch kleiner als Plaintext ({}) sein",
|
||||
ciphertext.len(),
|
||||
plaintext.len()
|
||||
);
|
||||
|
||||
let decrypted =
|
||||
decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lz4_chunk_compression_threshold() {
|
||||
let dek = generate_dek();
|
||||
// Unkomprimierbare Zufallsdaten (keine 64 Bytes Ersparnis)
|
||||
let mut random_bytes = vec![0u8; 1000];
|
||||
OsRng.fill_bytes(&mut random_bytes);
|
||||
|
||||
let (ct, nonce, tag) =
|
||||
encrypt_chunk(&dek, 1, 0, &random_bytes, FORMAT_VERSION_V2).unwrap();
|
||||
// Da Kompression keine 64 Bytes spart, wird COMPRESSION_NONE (1 Byte) + Plaintext gespeichert
|
||||
assert_eq!(ct.len(), random_bytes.len() + 1);
|
||||
|
||||
let decrypted = decrypt_chunk(&dek, 1, 0, &ct, &nonce, &tag, FORMAT_VERSION_V2).unwrap();
|
||||
assert_eq!(decrypted, random_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v1_backward_compatibility() {
|
||||
let dek = generate_dek();
|
||||
let plaintext = b"Uncompressed Legacy V1 Chunk Payload";
|
||||
let node_id = 5i64;
|
||||
let chunk_index = 0u32;
|
||||
|
||||
// V1 Format: Reine Verschlüsselung ohne Kompressionspräfix
|
||||
let (ciphertext, nonce, tag) =
|
||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V1).unwrap();
|
||||
assert_eq!(ciphertext.len(), plaintext.len());
|
||||
|
||||
let decrypted =
|
||||
decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V1).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bip39_recovery_phrase_roundtrip() {
|
||||
let dek = generate_dek();
|
||||
let mnemonic_str = dek_to_mnemonic(&dek).expect("Generate mnemonic");
|
||||
let words: Vec<&str> = mnemonic_str.split_whitespace().collect();
|
||||
assert_eq!(words.len(), 24, "Mnemonic must have exactly 24 words");
|
||||
|
||||
let recovered_dek = mnemonic_to_dek(&mnemonic_str).expect("Recover DEK");
|
||||
assert_eq!(*dek, *recovered_dek, "Recovered DEK must match original DEK");
|
||||
|
||||
// Whitespace-Toleranz (z. B. doppelte Leerzeichen, Zeilenumbrüche)
|
||||
let messy_phrase = format!(" {} \n\t {} ", words[0..12].join(" "), words[12..24].join(" \n "));
|
||||
let recovered_messy = mnemonic_to_dek(&messy_phrase).expect("Recover messy");
|
||||
assert_eq!(*dek, *recovered_messy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bip39_invalid_words_and_checksum() {
|
||||
// 1. Nicht im Wörterbuch enthaltenes Wort
|
||||
let invalid_word_phrase = "abandon amount anchor animal archive arm armed army armor arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow fakeinvalidword";
|
||||
assert!(mnemonic_to_dek(invalid_word_phrase).is_err());
|
||||
|
||||
// 2. Falsche Wortanzahl (z. B. 23 statt 24)
|
||||
let short_phrase = "abandon amount anchor animal archive arm armed army armor arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow arrow";
|
||||
assert!(mnemonic_to_dek(short_phrase).is_err());
|
||||
|
||||
// 3. Gültige Wörter, aber Prüfsumme ungültig (letztes Wort verändert)
|
||||
let dek = generate_dek();
|
||||
let mut words: Vec<String> = dek_to_mnemonic(&dek)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
// Tausche das letzte Wort gegen ein anderes gültiges BIP-39 Wort
|
||||
let original_last = words[23].clone();
|
||||
words[23] = if original_last == "abandon" { "zoo".to_string() } else { "abandon".to_string() };
|
||||
let corrupted_phrase = words.join(" ");
|
||||
assert!(mnemonic_to_dek(&corrupted_phrase).is_err(), "Checksum check must fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bip39_advanced_normalisation_and_suggestions() {
|
||||
let dek = generate_dek();
|
||||
let mnemonic_str = dek_to_mnemonic(&dek).unwrap();
|
||||
let words: Vec<&str> = mnemonic_str.split_whitespace().collect();
|
||||
|
||||
// 1. Großbuchstaben-Normalisierung (z.B. Smartphone Auto-Capitalization)
|
||||
let uppercase_phrase = mnemonic_str.to_uppercase();
|
||||
let recovered = mnemonic_to_dek(&uppercase_phrase).expect("Recover uppercase");
|
||||
assert_eq!(*dek, *recovered);
|
||||
|
||||
// 2. Satzzeichen & Trennzeichen (z.B. Kommas oder Punkte zwischen Wörtern)
|
||||
let punctuated = format!("{}.", words.join(", "));
|
||||
let recovered_punct = mnemonic_to_dek(&punctuated).expect("Recover punctuated");
|
||||
assert_eq!(*dek, *recovered_punct);
|
||||
|
||||
// 3. Typo-Vorschlag (Levenshtein)
|
||||
let mut typo_words = words.clone();
|
||||
typo_words[5] = "abondon"; // Typo for abandon
|
||||
let typo_phrase = typo_words.join(" ");
|
||||
let err = mnemonic_to_dek(&typo_phrase).unwrap_err().to_string();
|
||||
assert!(err.contains("Wort #6 'abondon' ist ungültig"));
|
||||
assert!(err.contains("Meinten Sie 'abandon'?"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_node_name_encryption_and_dummy_slot() {
|
||||
let dek = generate_dek();
|
||||
let filename = "ultra_geheimes_dokument.pdf";
|
||||
let parent_id = 2i64;
|
||||
let encrypted = encrypt_node_name(&dek, parent_id, filename);
|
||||
// Kein verräterisches Präfix mehr! Reines Hex.
|
||||
assert!(!encrypted.starts_with("$h$"));
|
||||
assert!(!encrypted.contains(filename));
|
||||
assert!(encrypted.len() >= 56);
|
||||
|
||||
let decrypted = decrypt_node_name(&dek, parent_id, &encrypted).expect("Decrypt name");
|
||||
assert_eq!(decrypted, filename);
|
||||
|
||||
// Abwärtskompatibilität: Legacy $h$<nonce>$<tag>$<ct> Format muss weiter entschlüsselt werden
|
||||
let legacy_format = format!("$h${}${}${}", &encrypted[0..24], &encrypted[24..56], &encrypted[56..]);
|
||||
let decrypted_legacy = decrypt_node_name(&dek, parent_id, &legacy_format).expect("Decrypt legacy $h$ name");
|
||||
assert_eq!(decrypted_legacy, filename);
|
||||
|
||||
// Echte statische AAD Legacy-Verschlüsselung (b"SANCTUM_NODE_NAME")
|
||||
let cipher = Aes256Gcm::new_from_slice(&dek[..]).unwrap();
|
||||
let mut static_buf = filename.as_bytes().to_vec();
|
||||
let static_nonce = [42u8; 12];
|
||||
let static_tag = cipher
|
||||
.encrypt_in_place_detached(Nonce::from_slice(&static_nonce), b"SANCTUM_NODE_NAME", &mut static_buf)
|
||||
.unwrap();
|
||||
let legacy_static_format = format!(
|
||||
"$h${}${}${}",
|
||||
hex::encode(static_nonce),
|
||||
hex::encode(static_tag),
|
||||
hex::encode(&static_buf)
|
||||
);
|
||||
let decrypted_static = decrypt_node_name(&dek, parent_id, &legacy_static_format).expect("Decrypt legacy static AAD name");
|
||||
assert_eq!(decrypted_static, filename);
|
||||
|
||||
// Mit anderem DEK schlägt Entschlüsselung fehl
|
||||
let other_dek = generate_dek();
|
||||
assert!(decrypt_node_name(&other_dek, parent_id, &encrypted).is_none());
|
||||
|
||||
// Dummy-Slot hat korrekte Längen (72 Bytes für Modell A)
|
||||
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
|
||||
assert_eq!(dummy_dek.len(), 72);
|
||||
assert_eq!(dummy_nonce.len(), 12);
|
||||
assert_eq!(dummy_tag.len(), 16);
|
||||
assert_eq!(dummy_salt.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_name_aad_parent_binding() {
|
||||
let dek = generate_dek();
|
||||
let enc_folder_a = encrypt_node_name(&dek, 10, "secrets.txt");
|
||||
let enc_folder_b = encrypt_node_name(&dek, 20, "passwords.txt");
|
||||
|
||||
// Gültige parent_ids entschlüsseln erfolgreich
|
||||
assert_eq!(decrypt_node_name(&dek, 10, &enc_folder_a).unwrap(), "secrets.txt");
|
||||
assert_eq!(decrypt_node_name(&dek, 20, &enc_folder_b).unwrap(), "passwords.txt");
|
||||
|
||||
// Swap-Angriff: Ein Angreifer verschiebt enc_folder_a in Ordner 20
|
||||
assert!(decrypt_node_name(&dek, 20, &enc_folder_a).is_none(), "Swap in anderen Ordner muss durch AAD fehlschlagen!");
|
||||
assert!(decrypt_node_name(&dek, 10, &enc_folder_b).is_none(), "Swap in anderen Ordner muss durch AAD fehlschlagen!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_a_slot_payloads() {
|
||||
let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap();
|
||||
let kek_1 = derive_kek("HiddenPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
let dek_1 = generate_dek();
|
||||
let carrier_node_id = 42i64;
|
||||
|
||||
// Slot 0 Payload: 40 Bytes
|
||||
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
assert_eq!(wrapped_0.len(), 40);
|
||||
|
||||
let unwrapped_0 = unwrap_key_payload(&kek_0, &wrapped_0, &nonce_0, &tag_0).unwrap();
|
||||
assert_eq!(unwrapped_0.len(), 40);
|
||||
assert_eq!(&unwrapped_0[0..32], &*dek_0);
|
||||
let recovered_cid_0 = i64::from_le_bytes(unwrapped_0[32..40].try_into().unwrap());
|
||||
assert_eq!(recovered_cid_0, carrier_node_id);
|
||||
|
||||
// Slot 1 Payload: 72 Bytes
|
||||
let (wrapped_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
assert_eq!(wrapped_1.len(), 72);
|
||||
|
||||
let unwrapped_1 = unwrap_key_payload(&kek_1, &wrapped_1, &nonce_1, &tag_1).unwrap();
|
||||
assert_eq!(unwrapped_1.len(), 72);
|
||||
assert_eq!(&unwrapped_1[0..32], &*dek_1);
|
||||
assert_eq!(&unwrapped_1[32..64], &*dek_0);
|
||||
let recovered_cid_1 = i64::from_le_bytes(unwrapped_1[64..72].try_into().unwrap());
|
||||
assert_eq!(recovered_cid_1, carrier_node_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lz4_decompression_bomb_protection() {
|
||||
use aes_gcm::KeyInit;
|
||||
let dek = generate_dek();
|
||||
let cipher = Aes256Gcm::new_from_slice(&*dek).unwrap();
|
||||
let node_id = 999;
|
||||
let chunk_index = 0;
|
||||
let aad = build_chunk_aad(node_id, chunk_index);
|
||||
|
||||
// Erstelle präparierte LZ4-Payload mit deklarierter Größe von 5 MB (> 1 MB CHUNK_SIZE)
|
||||
let mut malicious_plaintext = Vec::new();
|
||||
malicious_plaintext.push(COMPRESSION_LZ4);
|
||||
let fake_uncompressed_size: u32 = 5 * 1024 * 1024; // 5 MB
|
||||
malicious_plaintext.extend_from_slice(&fake_uncompressed_size.to_le_bytes());
|
||||
malicious_plaintext.extend_from_slice(&[0u8; 32]); // Dummy-LZ4-Payload
|
||||
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let mut ct = malicious_plaintext.clone();
|
||||
let tag = cipher.encrypt_in_place_detached(nonce, &aad, &mut ct).unwrap();
|
||||
let tag_bytes: [u8; 16] = tag.as_slice().try_into().unwrap();
|
||||
|
||||
// Entschlüsselung muss fehlschlagen, da Dekomprimierungs-Bomb-Schutz greift
|
||||
let res = decrypt_chunk(&dek, node_id, chunk_index, &ct, &nonce_bytes, &tag_bytes, FORMAT_VERSION_V2);
|
||||
assert!(res.is_err(), "Dekomprimierungs-Bomb über 1 MB muss abgewiesen werden!");
|
||||
let err_msg = res.err().unwrap().to_string();
|
||||
assert!(err_msg.contains("Decompression-Bomb Schutz ausgelöst"), "Fehlermeldung erwartet: {}", err_msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
pub mod carrier;
|
||||
pub mod crypto;
|
||||
pub mod mount;
|
||||
pub mod platform;
|
||||
pub mod recovery;
|
||||
pub mod storage;
|
||||
pub mod ui;
|
||||
pub mod verify;
|
||||
pub mod vfs;
|
||||
pub mod windows;
|
||||
|
||||
+888
-22
File diff suppressed because it is too large
Load Diff
+580
-69
@@ -2,6 +2,8 @@ use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use dav_server::{fakels::FakeLs, DavHandler};
|
||||
@@ -11,19 +13,29 @@ use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{debug, warn};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::crypto::{derive_kek, unwrap_dek};
|
||||
use crate::crypto::mnemonic_to_dek;
|
||||
use crate::storage::Database;
|
||||
use crate::ui;
|
||||
use crate::vfs::SanctumFs;
|
||||
|
||||
/// Authentifizierungsmethode für das Einbinden eines Containers: Entweder Master-Passwort oder 24-Wort Notfallschlüssel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ContainerAuth {
|
||||
Password(Zeroizing<String>),
|
||||
RecoveryKey(Zeroizing<String>),
|
||||
}
|
||||
|
||||
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
|
||||
pub fn format_drive(drive_letter: char) -> String {
|
||||
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<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let drive_str = format_drive(drive_letter);
|
||||
|
||||
let output = Command::new("net")
|
||||
@@ -44,38 +56,109 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = drive_letter;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
{
|
||||
let _ = drive_letter;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/ /persistent:no` ein.
|
||||
fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> {
|
||||
let url = format!("http://127.0.0.1:{}/", port);
|
||||
/// 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 output = Command::new("net")
|
||||
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{}{}",
|
||||
"Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}",
|
||||
drive_str,
|
||||
stdout,
|
||||
stderr
|
||||
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 ein.
|
||||
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk (Windows) bzw. VFS (Linux) ein.
|
||||
pub async fn mount_container(
|
||||
container_path: &Path,
|
||||
drive_letter: char,
|
||||
mount_point: Option<&Path>,
|
||||
requested_port: Option<u16>,
|
||||
password: &str,
|
||||
auth: ContainerAuth,
|
||||
open_explorer: bool,
|
||||
enable_tray: bool,
|
||||
idle_timeout: Option<u64>,
|
||||
lock_on_screen_lock: bool,
|
||||
anti_leak: bool,
|
||||
stealth: bool,
|
||||
) -> Result<()> {
|
||||
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() {
|
||||
bail!(
|
||||
@@ -84,8 +167,10 @@ pub async fn mount_container(
|
||||
);
|
||||
}
|
||||
|
||||
if !stealth {
|
||||
println!();
|
||||
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
||||
}
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
@@ -93,22 +178,66 @@ pub async fn mount_container(
|
||||
.read_meta()
|
||||
.context("Konnte Container-Header nicht lesen")?;
|
||||
|
||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
|
||||
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
||||
let (dek, carrier_dek, carrier_node_id, version, vault_id) = match auth {
|
||||
ContainerAuth::Password(ref password) => {
|
||||
if !stealth {
|
||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...");
|
||||
}
|
||||
match meta.authenticate(password) {
|
||||
Some(keys) => {
|
||||
if !stealth {
|
||||
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
|
||||
}
|
||||
let vault_id = keys.slot_id();
|
||||
let ver = keys.version();
|
||||
let c_dek = keys.carrier_dek();
|
||||
let c_nid = keys.carrier_node_id();
|
||||
(keys.0, c_dek, c_nid, ver, vault_id)
|
||||
}
|
||||
None => {
|
||||
bail!("Ungültiges Master-Passwort oder Container beschädigt");
|
||||
}
|
||||
}
|
||||
}
|
||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||
if !stealth {
|
||||
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
||||
}
|
||||
let dek = mnemonic_to_dek(phrase)
|
||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
if !stealth {
|
||||
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
||||
}
|
||||
|
||||
ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM...");
|
||||
let dek = unwrap_dek(
|
||||
&kek,
|
||||
&meta.wrapped_dek,
|
||||
&meta.header_nonce,
|
||||
&meta.header_tag,
|
||||
)
|
||||
.context("Ungültiges Master-Passwort oder Container beschädigt")?;
|
||||
// Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann
|
||||
let is_hidden = {
|
||||
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||
!children.is_empty()
|
||||
};
|
||||
let vault_id = if is_hidden { 1 } else { 0 };
|
||||
(dek, None, None, meta.version, vault_id)
|
||||
}
|
||||
};
|
||||
|
||||
// WebDAV Filesystem und Handler konfigurieren
|
||||
let fs = SanctumFs::new(db.clone(), dek);
|
||||
// 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren
|
||||
let mut token_bytes = [0u8; 16];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
|
||||
let session_token = hex::encode(token_bytes);
|
||||
let token_path_prefix = format!("/{}", session_token);
|
||||
|
||||
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Carrier-Routing)
|
||||
let fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
dek,
|
||||
carrier_dek,
|
||||
carrier_node_id,
|
||||
version,
|
||||
anti_leak,
|
||||
vault_id,
|
||||
);
|
||||
let last_activity = fs.last_activity();
|
||||
let dav_server = DavHandler::builder()
|
||||
.strip_prefix(token_path_prefix.clone())
|
||||
.filesystem(Box::new(fs))
|
||||
.locksystem(FakeLs::new())
|
||||
.build_handler();
|
||||
@@ -130,6 +259,8 @@ pub async fn mount_container(
|
||||
let bound_addr = listener.local_addr()?;
|
||||
let bound_port = bound_addr.port();
|
||||
|
||||
if !stealth {
|
||||
#[cfg(windows)]
|
||||
ui::step(
|
||||
4,
|
||||
4,
|
||||
@@ -139,74 +270,292 @@ pub async fn mount_container(
|
||||
bound_port, drive_str
|
||||
),
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
ui::step(
|
||||
4,
|
||||
4,
|
||||
"🌐",
|
||||
&format!(
|
||||
"Starte WebDAV-Server auf Port {} & initialisiere VFS...",
|
||||
bound_port
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
|
||||
// Hyper HTTP Server Loop im Hintergrund starten
|
||||
let server_dav = dav_server.clone();
|
||||
let server_handle = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
let (stream, _) = match res {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
warn!("Verbindungsfehler im TCP-Listener: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let server_handle = tokio::spawn(serve_webdav_loop(
|
||||
listener,
|
||||
dav_server,
|
||||
token_path_prefix.clone(),
|
||||
shutdown_rx,
|
||||
));
|
||||
|
||||
let io = TokioIo::new(stream);
|
||||
let handler = server_dav.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let service = service_fn(move |req| {
|
||||
let h = handler.clone();
|
||||
async move {
|
||||
Ok::<_, Infallible>(h.handle(req).await)
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
|
||||
// Client-Disconnects im Explorer sind normal
|
||||
debug!("HTTP-Verbindungsende: {:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
debug!("WebDAV-Server-Task empfängt Shutdown-Signal.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Netzlaufwerk einbinden
|
||||
if let Err(e) = run_net_use_mount(&drive_str, bound_port) {
|
||||
// Netzlaufwerk bzw. Verzeichnis einbinden
|
||||
if let Err(e) = run_mount_command(&drive_str, bound_port, &session_token) {
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
||||
#[cfg(windows)]
|
||||
if open_explorer {
|
||||
let _ = crate::windows::open_in_explorer(drive_letter);
|
||||
}
|
||||
|
||||
// System-Tray Initialisierung (nur Windows)
|
||||
#[cfg(windows)]
|
||||
let (_tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||
#[cfg(windows)]
|
||||
let _tray = if enable_tray {
|
||||
let icon_source = crate::windows::get_default_system_icon()
|
||||
.unwrap_or(tray_item::IconSource::Resource(""));
|
||||
let title = format!("Sanctum ({drive_str})");
|
||||
match tray_item::TrayItem::new(&title, icon_source) {
|
||||
Ok(mut tray) => {
|
||||
let container_name = container_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let _ = tray.add_label(&format!("Sanctum: {drive_str} ({container_name})"));
|
||||
let dl = drive_letter;
|
||||
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
||||
let _ = crate::windows::open_in_explorer(dl);
|
||||
});
|
||||
let s_tx = _tray_shutdown_tx.clone();
|
||||
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
||||
let _ = s_tx.blocking_send(());
|
||||
});
|
||||
Some(tray)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("System-Tray Icon konnte nicht erstellt werden: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Inaktivitäts-Timer (Auto-Lock)
|
||||
let (idle_shutdown_tx, mut idle_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||
if let Some(timeout_secs) = idle_timeout {
|
||||
if timeout_secs > 0 {
|
||||
let last_act = last_activity.clone();
|
||||
let idle_tx = idle_shutdown_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let last = last_act.load(Ordering::Relaxed);
|
||||
if now.saturating_sub(last) >= timeout_secs {
|
||||
let _ = idle_tx.send(()).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
|
||||
#[cfg(windows)]
|
||||
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||
#[cfg(windows)]
|
||||
let _session_monitor = if lock_on_screen_lock {
|
||||
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
||||
Ok(guard) => Some(guard),
|
||||
Err(e) => {
|
||||
warn!("Konnte Windows Session-Lock Monitor nicht aktivieren: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if stealth {
|
||||
#[cfg(windows)]
|
||||
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
|
||||
#[cfg(not(windows))]
|
||||
println!("Sanctum: WebDAV-Server auf Port {} bereit.", bound_port);
|
||||
} else {
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
#[cfg(windows)]
|
||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
||||
#[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);
|
||||
#[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 {
|
||||
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if lock_on_screen_lock {
|
||||
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
|
||||
}
|
||||
if anti_leak {
|
||||
#[cfg(windows)]
|
||||
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 {
|
||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
||||
}
|
||||
println!();
|
||||
println!(" [{}] Drücke [Ctrl+C] zum sicheren Trennen und Schließen.", ui::yellow("Tipp"));
|
||||
#[cfg(windows)]
|
||||
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!();
|
||||
}
|
||||
|
||||
// Warten auf Strg+C
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?;
|
||||
// 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);
|
||||
#[cfg(windows)]
|
||||
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)
|
||||
#[cfg(windows)]
|
||||
tokio::select! {
|
||||
res = tokio::signal::ctrl_c() => {
|
||||
let _ = res;
|
||||
if !stealth {
|
||||
println!();
|
||||
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
Some(()) = console_close_rx.recv() => {
|
||||
if !stealth {
|
||||
println!();
|
||||
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
Some(()) = tray_shutdown_rx.recv() => {
|
||||
if !stealth {
|
||||
println!();
|
||||
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
Some(()) = session_lock_rx.recv() => {
|
||||
if !stealth {
|
||||
println!();
|
||||
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
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 {
|
||||
println!();
|
||||
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stealth {
|
||||
let _ = unmount_drive(drive_letter);
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
if let Ok(freelist) = db.freelist_count() {
|
||||
if freelist > 0 {
|
||||
let _ = db.incremental_vacuum(None);
|
||||
}
|
||||
}
|
||||
let _ = db.checkpoint();
|
||||
#[cfg(windows)]
|
||||
println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str);
|
||||
#[cfg(not(windows))]
|
||||
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
@@ -216,11 +565,30 @@ pub async fn mount_container(
|
||||
} 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"));
|
||||
}
|
||||
|
||||
// HTTP Server beenden
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
|
||||
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
|
||||
if let Ok(freelist) = db.freelist_count() {
|
||||
if freelist > 0 {
|
||||
print!(" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
match db.incremental_vacuum(None) {
|
||||
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
|
||||
Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite WAL Checkpoint erzwingen
|
||||
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
@@ -233,6 +601,149 @@ pub async fn mount_container(
|
||||
println!();
|
||||
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔"));
|
||||
println!();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Überprüft, ob ein Host-Header auf Loopback (127.0.0.1, localhost, [::1]) zeigt (Anti-DNS-Rebinding).
|
||||
pub fn is_loopback_host(host_str: &str) -> bool {
|
||||
let host_lower = host_str.to_ascii_lowercase();
|
||||
host_lower.starts_with("127.0.0.1")
|
||||
|| host_lower.starts_with("localhost")
|
||||
|| host_lower.starts_with("[::1]")
|
||||
}
|
||||
|
||||
/// Maximale Anzahl gleichzeitiger Verbindungen zum lokalen WebDAV-Endpunkt (Schutz gegen Socket-Exhaustion).
|
||||
const MAX_CONCURRENT_DAV_CONNECTIONS: usize = 64;
|
||||
|
||||
/// Timeout für das Lesen von HTTP-Headern (Schutz gegen Slowloris-Angriffe auf Loopback).
|
||||
const HTTP_HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
|
||||
/// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Session-Token & Host-Header Sicherheits-Middleware aus.
|
||||
pub async fn serve_webdav_loop(
|
||||
listener: TcpListener,
|
||||
dav_server: DavHandler,
|
||||
token_path_prefix: String,
|
||||
mut shutdown_rx: watch::Receiver<bool>,
|
||||
) {
|
||||
let conn_semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_DAV_CONNECTIONS));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
let (stream, _) = match res {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
warn!("Verbindungsfehler im TCP-Listener: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// RT-01: Schutz gegen Connection-Starvation / Socket-Flooding (CWE-400)
|
||||
let permit = match conn_semaphore.clone().try_acquire_owned() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"WebDAV-Verbindungslimit ({} aktive Verbindungen) erreicht: Wehre potenziellen Connection-Starvation-Angriff ab.",
|
||||
MAX_CONCURRENT_DAV_CONNECTIONS
|
||||
);
|
||||
drop(stream);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let io = TokioIo::new(stream);
|
||||
let handler = dav_server.clone();
|
||||
let expected_prefix = token_path_prefix.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _permit = permit; // Permit wird bei Verbindungsende automatisch freigegeben
|
||||
|
||||
let service = service_fn(move |req| {
|
||||
let h = handler.clone();
|
||||
let prefix = expected_prefix.clone();
|
||||
async move {
|
||||
// 1. RT-02: Strikte Fail-Closed Host-Header Validierung (Anti-DNS-Rebinding & Anti-Spoofing)
|
||||
let host_valid = match req.headers().get(hyper::header::HOST) {
|
||||
Some(host_val) => match host_val.to_str() {
|
||||
Ok(host_str) => is_loopback_host(host_str),
|
||||
Err(_) => false,
|
||||
},
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !host_valid {
|
||||
warn!(
|
||||
"Abgewiesener Zugriff: Fehlender, ungültiger oder externer Host-Header ({:?})",
|
||||
req.headers().get(hyper::header::HOST)
|
||||
);
|
||||
let res = hyper::Response::builder()
|
||||
.status(hyper::StatusCode::FORBIDDEN)
|
||||
.body(dav_server::body::Body::empty())
|
||||
.unwrap();
|
||||
return Ok::<_, Infallible>(res);
|
||||
}
|
||||
|
||||
// 2. Session-Token Pfadprüfung (Loopback-Schutz gegen unbefugte lokale Prozesse & Browser CSRF)
|
||||
let path = req.uri().path();
|
||||
if !path.starts_with(&prefix) {
|
||||
debug!("Abgewiesener Zugriff ohne gültiges Session-Token: {}", path);
|
||||
let res = hyper::Response::builder()
|
||||
.status(hyper::StatusCode::FORBIDDEN)
|
||||
.body(dav_server::body::Body::empty())
|
||||
.unwrap();
|
||||
return Ok::<_, Infallible>(res);
|
||||
}
|
||||
|
||||
Ok::<_, Infallible>(h.handle(req).await)
|
||||
}
|
||||
});
|
||||
|
||||
// RT-01: Header-Read-Timeout & TokioTimer zur aktiven Abwehr von Slowloris-Hanging-Sockets
|
||||
let mut builder = http1::Builder::new();
|
||||
builder.timer(hyper_util::rt::TokioTimer::new());
|
||||
builder.header_read_timeout(HTTP_HEADER_READ_TIMEOUT);
|
||||
|
||||
if let Err(err) = builder.serve_connection(io, service).await {
|
||||
// Client-Disconnects im Explorer oder Timeout-Drops sind normal
|
||||
debug!("HTTP-Verbindungsende: {:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
debug!("WebDAV-Server-Task empfängt Shutdown-Signal.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_loopback_host_validation() {
|
||||
assert!(is_loopback_host("127.0.0.1"));
|
||||
assert!(is_loopback_host("127.0.0.1:8443"));
|
||||
assert!(is_loopback_host("localhost"));
|
||||
assert!(is_loopback_host("localhost:8443"));
|
||||
assert!(is_loopback_host("[::1]"));
|
||||
assert!(is_loopback_host("[::1]:8443"));
|
||||
|
||||
// Abweisung externer Hosts oder DNS-Rebinding-Attacken
|
||||
assert!(!is_loopback_host("evil.com"));
|
||||
assert!(!is_loopback_host("attacker.local"));
|
||||
assert!(!is_loopback_host("192.168.1.50"));
|
||||
assert!(!is_loopback_host("10.0.0.1"));
|
||||
assert!(!is_loopback_host(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_drive() {
|
||||
assert_eq!(format_drive('s'), "S:");
|
||||
assert_eq!(format_drive('Z'), "Z:");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::{
|
||||
derive_kek, generate_dummy_slot, mnemonic_to_dek, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, FORMAT_VERSION,
|
||||
};
|
||||
use crate::storage::{ContainerMeta, Database, SlotMeta};
|
||||
|
||||
pub const HEADER_BACKUP_MAGIC: &str = "SANCTUM_HEADER_BACKUP";
|
||||
pub const CURRENT_BACKUP_VERSION: u32 = 1;
|
||||
|
||||
/// Struktur für einen gesicherten Header-Slot.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlotBackup {
|
||||
pub slot_id: u32,
|
||||
pub version: u32,
|
||||
pub kdf_salt_hex: String,
|
||||
pub kdf_params: KdfParams,
|
||||
pub wrapped_dek_hex: String,
|
||||
pub header_nonce_hex: String,
|
||||
pub header_tag_hex: String,
|
||||
}
|
||||
|
||||
/// Struktur für exportierte Header-Backups (.sanctum.hdr) im JSON-Format.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HeaderBackup {
|
||||
pub magic: String,
|
||||
pub backup_version: u32,
|
||||
pub container_format_version: u32,
|
||||
pub kdf_salt_hex: String,
|
||||
pub kdf_params: KdfParams,
|
||||
pub wrapped_dek_hex: String,
|
||||
pub header_nonce_hex: String,
|
||||
pub header_tag_hex: String,
|
||||
#[serde(default)]
|
||||
pub slots: Vec<SlotBackup>,
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
impl HeaderBackup {
|
||||
pub fn from_meta(meta: &ContainerMeta) -> Self {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
let slots: Vec<SlotBackup> = meta
|
||||
.slots
|
||||
.iter()
|
||||
.map(|s| SlotBackup {
|
||||
slot_id: s.slot_id,
|
||||
version: s.version,
|
||||
kdf_salt_hex: hex::encode(s.kdf_salt),
|
||||
kdf_params: s.kdf_params.clone(),
|
||||
wrapped_dek_hex: hex::encode(&s.wrapped_dek),
|
||||
header_nonce_hex: hex::encode(s.header_nonce),
|
||||
header_tag_hex: hex::encode(s.header_tag),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
magic: HEADER_BACKUP_MAGIC.to_string(),
|
||||
backup_version: CURRENT_BACKUP_VERSION,
|
||||
container_format_version: meta.version,
|
||||
kdf_salt_hex: hex::encode(meta.kdf_salt),
|
||||
kdf_params: meta.kdf_params.clone(),
|
||||
wrapped_dek_hex: hex::encode(&meta.wrapped_dek),
|
||||
header_nonce_hex: hex::encode(meta.header_nonce),
|
||||
header_tag_hex: hex::encode(meta.header_tag),
|
||||
slots,
|
||||
created_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_meta(&self) -> Result<ContainerMeta> {
|
||||
if self.magic != HEADER_BACKUP_MAGIC {
|
||||
bail!("Ungültige Header-Backup-Datei: Falsches Magic-Präfix");
|
||||
}
|
||||
|
||||
let mut slots = Vec::new();
|
||||
|
||||
if !self.slots.is_empty() {
|
||||
for s in &self.slots {
|
||||
let salt_bytes = hex::decode(&s.kdf_salt_hex)
|
||||
.context("Ungültige Hex-Kodierung für KDF-Salt")?;
|
||||
if salt_bytes.len() != 16 {
|
||||
bail!("Ungültige Salt-Länge im Backup: erwartet 16 Bytes, erhalten {}", salt_bytes.len());
|
||||
}
|
||||
let mut kdf_salt = [0u8; 16];
|
||||
kdf_salt.copy_from_slice(&salt_bytes);
|
||||
|
||||
let wrapped_dek = hex::decode(&s.wrapped_dek_hex)
|
||||
.context("Ungültige Hex-Kodierung für wrapped_dek")?;
|
||||
|
||||
let nonce_bytes = hex::decode(&s.header_nonce_hex)
|
||||
.context("Ungültige Hex-Kodierung für Header-Nonce")?;
|
||||
if nonce_bytes.len() != 12 {
|
||||
bail!("Ungültige Nonce-Länge im Backup: erwartet 12 Bytes, erhalten {}", nonce_bytes.len());
|
||||
}
|
||||
let mut header_nonce = [0u8; 12];
|
||||
header_nonce.copy_from_slice(&nonce_bytes);
|
||||
|
||||
let tag_bytes = hex::decode(&s.header_tag_hex)
|
||||
.context("Ungültige Hex-Kodierung für Header-Tag")?;
|
||||
if tag_bytes.len() != 16 {
|
||||
bail!("Ungültige Tag-Länge im Backup: erwartet 16 Bytes, erhalten {}", tag_bytes.len());
|
||||
}
|
||||
let mut header_tag = [0u8; 16];
|
||||
header_tag.copy_from_slice(&tag_bytes);
|
||||
|
||||
slots.push(SlotMeta {
|
||||
slot_id: s.slot_id,
|
||||
version: s.version,
|
||||
kdf_salt,
|
||||
kdf_params: s.kdf_params.clone(),
|
||||
wrapped_dek,
|
||||
header_nonce,
|
||||
header_tag,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback für alte Backups ohne slots-Array
|
||||
let salt_bytes = hex::decode(&self.kdf_salt_hex)
|
||||
.context("Ungültige Hex-Kodierung für KDF-Salt")?;
|
||||
if salt_bytes.len() != 16 {
|
||||
bail!("Ungültige Salt-Länge im Backup: erwartet 16 Bytes, erhalten {}", salt_bytes.len());
|
||||
}
|
||||
let mut kdf_salt = [0u8; 16];
|
||||
kdf_salt.copy_from_slice(&salt_bytes);
|
||||
|
||||
let wrapped_dek = hex::decode(&self.wrapped_dek_hex)
|
||||
.context("Ungültige Hex-Kodierung für wrapped_dek")?;
|
||||
|
||||
let nonce_bytes = hex::decode(&self.header_nonce_hex)
|
||||
.context("Ungültige Hex-Kodierung für Header-Nonce")?;
|
||||
if nonce_bytes.len() != 12 {
|
||||
bail!("Ungültige Nonce-Länge im Backup: erwartet 12 Bytes, erhalten {}", nonce_bytes.len());
|
||||
}
|
||||
let mut header_nonce = [0u8; 12];
|
||||
header_nonce.copy_from_slice(&nonce_bytes);
|
||||
|
||||
let tag_bytes = hex::decode(&self.header_tag_hex)
|
||||
.context("Ungültige Hex-Kodierung für Header-Tag")?;
|
||||
if tag_bytes.len() != 16 {
|
||||
bail!("Ungültige Tag-Länge im Backup: erwartet 16 Bytes, erhalten {}", tag_bytes.len());
|
||||
}
|
||||
let mut header_tag = [0u8; 16];
|
||||
header_tag.copy_from_slice(&tag_bytes);
|
||||
|
||||
let slot0 = SlotMeta {
|
||||
slot_id: 0,
|
||||
version: self.container_format_version,
|
||||
kdf_salt,
|
||||
kdf_params: self.kdf_params.clone(),
|
||||
wrapped_dek: wrapped_dek.clone(),
|
||||
header_nonce,
|
||||
header_tag,
|
||||
};
|
||||
slots.push(slot0);
|
||||
}
|
||||
|
||||
let slot0 = &slots[0];
|
||||
Ok(ContainerMeta {
|
||||
version: slot0.version,
|
||||
kdf_salt: slot0.kdf_salt,
|
||||
kdf_params: slot0.kdf_params.clone(),
|
||||
wrapped_dek: slot0.wrapped_dek.clone(),
|
||||
header_nonce: slot0.header_nonce,
|
||||
header_tag: slot0.header_tag,
|
||||
slots,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sichert die Header-Metadaten eines Containers in eine externe Backup-Datei (.sanctum.hdr).
|
||||
pub fn export_header_backup(container_path: &Path, backup_path: &Path) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank zum Lesen des Headers nicht öffnen")?;
|
||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||
|
||||
let backup = HeaderBackup::from_meta(&meta);
|
||||
let json_data = serde_json::to_string_pretty(&backup)
|
||||
.context("Fehler beim Serialisieren des Header-Backups")?;
|
||||
|
||||
fs::write(backup_path, json_data)
|
||||
.with_context(|| format!("Konnte Backup-Datei '{}' nicht schreiben", backup_path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stellt den Container-Header aus einer Sicherungsdatei (.sanctum.hdr) wieder her.
|
||||
pub fn restore_header_backup(container_path: &Path, backup_path: &Path) -> Result<()> {
|
||||
if !backup_path.exists() {
|
||||
bail!("Backup-Datei '{}' existiert nicht.", backup_path.display());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(backup_path)
|
||||
.with_context(|| format!("Konnte Backup-Datei '{}' nicht lesen", backup_path.display()))?;
|
||||
|
||||
let backup: HeaderBackup = serde_json::from_str(&content)
|
||||
.context("Ungültiges Backup-Dateiformat (JSON-Parsing fehlgeschlagen)")?;
|
||||
|
||||
let meta = backup.to_meta()?;
|
||||
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Ziel-Containerdatei nicht öffnen")?;
|
||||
|
||||
db.restore_meta(&meta)
|
||||
.context("Fehler beim Wiederherstellen der Header-Tabelle in der Datenbank")?;
|
||||
|
||||
db.checkpoint().context("Fehler beim WAL-Checkpoint nach Header-Wiederherstellung")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rekonstruiert einen Slot des Container-Headers mithilfe des 24-Wort BIP-39 Notfallschlüssels
|
||||
/// und initialisiert ein neues Master-Passwort für den entsprechenden Slot (0 = Decoy, 1 = Hidden Vault).
|
||||
pub fn restore_slot_from_recovery_key(
|
||||
container_path: &Path,
|
||||
recovery_key: &str,
|
||||
new_password: &str,
|
||||
target_slot_id: u32,
|
||||
) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das neue Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
|
||||
// 1. DEK aus 24-Wort-Phrase dekodieren & validieren
|
||||
let dek = mnemonic_to_dek(recovery_key)
|
||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
|
||||
// 2. Neuen KEK mit frischem Salt ableiten
|
||||
let mut salt = [0u8; 16];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut salt);
|
||||
|
||||
let kdf_params = KdfParams::default();
|
||||
let kek = derive_kek(new_password, &salt, &kdf_params)
|
||||
.context("Schlüsselableitung für neues Passwort fehlgeschlagen")?;
|
||||
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0);
|
||||
|
||||
let mut existing_slots = db.read_slots().unwrap_or_default();
|
||||
|
||||
let new_slot = if target_slot_id == 1 {
|
||||
// Für Slot 1 wird DEK_0 benötigt (z. B. aus vorhandenem Slot 0)
|
||||
let dek_0_bytes = [0u8; 32];
|
||||
let (wrapped, nonce, tag) = wrap_slot1_payload(&kek, &dek, &dek_0_bytes, carrier_node_id)?;
|
||||
SlotMeta {
|
||||
slot_id: 1,
|
||||
version: FORMAT_VERSION,
|
||||
kdf_salt: salt,
|
||||
kdf_params: kdf_params.clone(),
|
||||
wrapped_dek: wrapped,
|
||||
header_nonce: nonce,
|
||||
header_tag: tag,
|
||||
}
|
||||
} else {
|
||||
// Slot 0 (Standard / Decoy Vault) mit 40 Bytes für Modell A
|
||||
let (wrapped, nonce, tag) = wrap_slot0_payload(&kek, &dek, carrier_node_id)?;
|
||||
SlotMeta {
|
||||
slot_id: 0,
|
||||
version: FORMAT_VERSION,
|
||||
kdf_salt: salt,
|
||||
kdf_params: kdf_params.clone(),
|
||||
wrapped_dek: wrapped,
|
||||
header_nonce: nonce,
|
||||
header_tag: tag,
|
||||
}
|
||||
};
|
||||
|
||||
// Slot ersetzen bzw. einfügen
|
||||
existing_slots.retain(|s| s.slot_id != target_slot_id);
|
||||
existing_slots.push(new_slot);
|
||||
existing_slots.sort_by_key(|s| s.slot_id);
|
||||
|
||||
// Falls Slot 1 fehlt, Dummy-Slot 1 ergänzen für Plausible Deniability
|
||||
if !existing_slots.iter().any(|s| s.slot_id == 1) {
|
||||
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
|
||||
let dummy_slot = SlotMeta {
|
||||
slot_id: 1,
|
||||
version: FORMAT_VERSION,
|
||||
kdf_salt: dummy_salt,
|
||||
kdf_params: KdfParams::default(),
|
||||
wrapped_dek: dummy_dek,
|
||||
header_nonce: dummy_nonce,
|
||||
header_tag: dummy_tag,
|
||||
};
|
||||
existing_slots.push(dummy_slot);
|
||||
}
|
||||
|
||||
let slot0_meta = existing_slots
|
||||
.iter()
|
||||
.find(|s| s.slot_id == 0)
|
||||
.ok_or_else(|| anyhow::anyhow!("Slot 0 fehlt im Header"))?;
|
||||
|
||||
let meta = ContainerMeta {
|
||||
version: FORMAT_VERSION,
|
||||
kdf_salt: slot0_meta.kdf_salt,
|
||||
kdf_params: slot0_meta.kdf_params.clone(),
|
||||
wrapped_dek: slot0_meta.wrapped_dek.clone(),
|
||||
header_nonce: slot0_meta.header_nonce,
|
||||
header_tag: slot0_meta.header_tag,
|
||||
slots: existing_slots,
|
||||
};
|
||||
|
||||
db.restore_meta(&meta)
|
||||
.context("Fehler beim Schreiben des rekonstruierten Headers")?;
|
||||
|
||||
db.checkpoint().context("Fehler beim WAL-Checkpoint nach Header-Rekonstruktion")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rekonstruiert den Decoy-Slot (Slot 0) mithilfe des 24-Wort BIP-39 Notfallschlüssels.
|
||||
pub fn restore_header_from_recovery_key(
|
||||
container_path: &Path,
|
||||
recovery_key: &str,
|
||||
new_password: &str,
|
||||
) -> Result<()> {
|
||||
restore_slot_from_recovery_key(container_path, recovery_key, new_password, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::{generate_dek, generate_salt, unwrap_dek, wrap_dek};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_header_backup_and_restore() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf = temp_dir.join(format!("test_backup_{}.sanctum", std::process::id()));
|
||||
let backup_path: PathBuf = temp_dir.join(format!("test_backup_{}.sanctum.hdr", std::process::id()));
|
||||
|
||||
if container_path.exists() {
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
if backup_path.exists() {
|
||||
let _ = fs::remove_file(&backup_path);
|
||||
}
|
||||
|
||||
let password = "SuperSecretPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
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.checkpoint().unwrap();
|
||||
|
||||
// 1. Export
|
||||
export_header_backup(&container_path, &backup_path).expect("Export header");
|
||||
assert!(backup_path.exists());
|
||||
|
||||
// 2. Header in DB gezielt zerstören/löschen
|
||||
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||
conn.execute("DELETE FROM meta", []).unwrap();
|
||||
drop(conn);
|
||||
|
||||
// Prüfen, dass Container jetzt unlesbar ist
|
||||
let broken_db = Database::open(&container_path).unwrap();
|
||||
assert!(broken_db.read_meta().is_err());
|
||||
drop(broken_db);
|
||||
|
||||
// 3. Restore
|
||||
restore_header_backup(&container_path, &backup_path).expect("Restore header");
|
||||
|
||||
// 4. Verifikation: Container wieder voll entschlüsselbar
|
||||
let restored_db = Database::open(&container_path).unwrap();
|
||||
let meta = restored_db.read_meta().expect("Read restored meta");
|
||||
let restored_kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params).unwrap();
|
||||
let active_dek = unwrap_dek(&restored_kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag)
|
||||
.expect("Unwrap restored DEK");
|
||||
assert_eq!(*dek, *active_dek);
|
||||
|
||||
let _ = fs::remove_file(&container_path);
|
||||
let _ = fs::remove_file(&backup_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restore_from_recovery_key() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf = temp_dir.join(format!("test_rec_key_{}.sanctum", std::process::id()));
|
||||
|
||||
if container_path.exists() {
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let old_password = "ForgottenOldPassword!";
|
||||
let new_password = "BrandNewRescuedPassword2026!";
|
||||
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
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();
|
||||
|
||||
// 24-Wort Notfallschlüssel sichern
|
||||
let phrase = crate::crypto::dek_to_mnemonic(&dek).unwrap();
|
||||
|
||||
let db = Database::open(&container_path).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Header zerstören
|
||||
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||
conn.execute("DELETE FROM meta", []).unwrap();
|
||||
drop(conn);
|
||||
|
||||
// Mit 24-Wort Schlüssel und NEUEM Passwort wiederherstellen
|
||||
restore_header_from_recovery_key(&container_path, &phrase, new_password)
|
||||
.expect("Restore from recovery key");
|
||||
|
||||
// Verifikation: Neues Passwort entschlüsselt originalen DEK
|
||||
let rescued_db = Database::open(&container_path).unwrap();
|
||||
let meta = rescued_db.read_meta().expect("Read rescued meta");
|
||||
let new_kek = derive_kek(new_password, &meta.kdf_salt, &meta.kdf_params).unwrap();
|
||||
let unwrapped = unwrap_dek(&new_kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag)
|
||||
.expect("Unwrap rescued DEK");
|
||||
assert_eq!(*dek, *unwrapped);
|
||||
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
}
|
||||
+1344
-85
File diff suppressed because it is too large
Load Diff
@@ -92,8 +92,114 @@ pub fn red(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn magenta(s: &str) -> String {
|
||||
if is_vt_enabled() {
|
||||
format!("\x1b[1;35m{s}\x1b[0m")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
||||
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
||||
let tag = cyan(&format!("[{}/{}]", num, total));
|
||||
println!(" {} {} {}", tag, icon, msg);
|
||||
}
|
||||
|
||||
/// Gibt den 24-Wort BIP-39 Notfall-Wiederherstellungsschlüssel in einer hervorgehobenen Sicherheitsbox aus.
|
||||
pub fn print_recovery_phrase_card(phrase: &str) {
|
||||
let words: Vec<&str> = phrase.split_whitespace().collect();
|
||||
println!();
|
||||
println!("{}", yellow("┌─────────────────────────────────────────────────────────────┐"));
|
||||
println!("{}", yellow("│ ⚠️ 24-WORT NOTFALL-WIEDERHERSTELLUNGSSCHLÜSSEL │"));
|
||||
println!("{}", yellow("├─────────────────────────────────────────────────────────────┤"));
|
||||
println!("│ Falls Sie Ihr Master-Passwort vergessen oder der Header │");
|
||||
println!("│ beschädigt wird, ist dies Ihre EINZIGE Rettung! │");
|
||||
println!("│ Notieren Sie die Wörter in EXAKTER Reihenfolge auf Papier! │");
|
||||
println!("{}", yellow("├─────────────────────────────────────────────────────────────┤"));
|
||||
|
||||
for row in 0..8 {
|
||||
let w1 = if row < words.len() {
|
||||
format!("{:2}. {:<11}", row + 1, words[row])
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let w2 = if row + 8 < words.len() {
|
||||
format!("{:2}. {:<11}", row + 9, words[row + 8])
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let w3 = if row + 16 < words.len() {
|
||||
format!("{:2}. {:<11}", row + 17, words[row + 16])
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
println!("│ {:<18} {:<18} {:<18} │", cyan(&w1), cyan(&w2), cyan(&w3));
|
||||
}
|
||||
|
||||
println!("{}", yellow("└─────────────────────────────────────────────────────────────┘"));
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Gibt den detaillierten Bericht einer Container-Integritätsprüfung aus.
|
||||
pub fn print_verification_report(report: &crate::verify::VerificationReport) {
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum Container-Integritätsprüfung (FSCK) │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Container: {}", report.container_path);
|
||||
println!(" Format: Version {}", report.format_version);
|
||||
println!();
|
||||
|
||||
let sqlite_status = if report.sqlite_ok {
|
||||
green("✔ OK")
|
||||
} else {
|
||||
red("✖ FEHLER")
|
||||
};
|
||||
let header_status = if report.header_ok {
|
||||
green("✔ OK")
|
||||
} else {
|
||||
red("✖ BESCHÄDIGT")
|
||||
};
|
||||
let tree_status = if report.orphan_nodes == 0 {
|
||||
green("✔ KONSISTENT")
|
||||
} else {
|
||||
red("✖ INKONSISTENT")
|
||||
};
|
||||
let chunk_status = if report.corrupted_chunks == 0 {
|
||||
green("✔ AUTHENTIFIZIERT")
|
||||
} else {
|
||||
red("✖ BESCHÄDIGT")
|
||||
};
|
||||
|
||||
println!(" • SQLite B-Tree Integrität: {}", sqlite_status);
|
||||
println!(" • Header & KDF-Metadaten: {}", header_status);
|
||||
println!(" • Verzeichnisbaum & Inodes: {}", tree_status);
|
||||
println!(" • AEAD Chunk-Authentizität: {}", chunk_status);
|
||||
println!();
|
||||
println!(" Statistiken:");
|
||||
println!(" - Ordner: {}", report.total_dirs);
|
||||
println!(" - Dateien: {}", report.total_files);
|
||||
println!(" - Daten-Chunks: {}", report.total_chunks);
|
||||
if report.total_bytes_decrypted > 0 {
|
||||
let mb = report.total_bytes_decrypted as f64 / (1024.0 * 1024.0);
|
||||
println!(" - Verifiziert: {:.2} MB (vollständig entschlüsselt)", mb);
|
||||
}
|
||||
|
||||
if !report.errors.is_empty() {
|
||||
println!();
|
||||
println!("{}", red(" Gefundene Probleme / Fehler:"));
|
||||
for err in &report.errors {
|
||||
println!(" {} {}", red("✖"), err);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if report.is_healthy() {
|
||||
println!(" {}", green("✔ Keine Beschädigungen oder Bitrot festgestellt. Der Container ist integer."));
|
||||
} else {
|
||||
println!(" {}", red("✖ ACHTUNG: Der Container weist Beschädigungen auf! Bitte Backup prüfen."));
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::crypto::{decrypt_chunk, FORMAT_VERSION_V1, FORMAT_VERSION_V2};
|
||||
use crate::storage::Database;
|
||||
|
||||
/// Bericht über das Ergebnis einer Container-Integritätsprüfung.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerificationReport {
|
||||
pub container_path: String,
|
||||
pub format_version: u32,
|
||||
pub sqlite_ok: bool,
|
||||
pub sqlite_errors: Vec<String>,
|
||||
pub header_ok: bool,
|
||||
pub header_error: Option<String>,
|
||||
pub total_nodes: usize,
|
||||
pub total_dirs: usize,
|
||||
pub total_files: usize,
|
||||
pub total_chunks: usize,
|
||||
pub total_bytes_decrypted: u64,
|
||||
pub corrupted_chunks: usize,
|
||||
pub orphan_nodes: usize,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl VerificationReport {
|
||||
pub fn is_healthy(&self) -> bool {
|
||||
self.sqlite_ok
|
||||
&& self.header_ok
|
||||
&& self.corrupted_chunks == 0
|
||||
&& self.orphan_nodes == 0
|
||||
&& self.errors.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt eine detaillierte Integritäts- und Bitrot-Prüfung auf einem Sanctum-Container durch.
|
||||
pub fn verify_container(
|
||||
container_path: &Path,
|
||||
dek: Option<&Zeroizing<[u8; 32]>>,
|
||||
full_chunks: bool,
|
||||
) -> Result<VerificationReport> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
let mut report = VerificationReport {
|
||||
container_path: container_path.display().to_string(),
|
||||
format_version: 0,
|
||||
sqlite_ok: true,
|
||||
sqlite_errors: Vec::new(),
|
||||
header_ok: true,
|
||||
header_error: None,
|
||||
total_nodes: 0,
|
||||
total_dirs: 0,
|
||||
total_files: 0,
|
||||
total_chunks: 0,
|
||||
total_bytes_decrypted: 0,
|
||||
corrupted_chunks: 0,
|
||||
orphan_nodes: 0,
|
||||
errors: Vec::new(),
|
||||
};
|
||||
|
||||
// 1. SQLite B-Tree & Foreign Key Prüfung
|
||||
let sqlite_issues = db.run_sqlite_integrity_check()
|
||||
.context("Fehler bei der Ausführung des SQLite integrity_check")?;
|
||||
if !sqlite_issues.is_empty() {
|
||||
report.sqlite_ok = false;
|
||||
report.sqlite_errors = sqlite_issues;
|
||||
}
|
||||
|
||||
// 2. Header & Magic Bytes Prüfung
|
||||
let meta = match db.read_meta() {
|
||||
Ok(m) => {
|
||||
report.format_version = m.version;
|
||||
if m.version != FORMAT_VERSION_V1 && m.version != FORMAT_VERSION_V2 {
|
||||
report.header_ok = false;
|
||||
report.header_error = Some(format!("Unbekannte Formatversion: {}", m.version));
|
||||
}
|
||||
if m.kdf_salt.len() != 16 {
|
||||
report.header_ok = false;
|
||||
report.header_error = Some("Ungültige KDF-Salt-Länge".to_string());
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
Err(e) => {
|
||||
report.header_ok = false;
|
||||
report.header_error = Some(format!("{e}"));
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Node-Hierarchie & Strukturprüfung
|
||||
let (dirs, files, chunks_count) = db.count_nodes_and_chunks()
|
||||
.context("Fehler beim Zählen der Knoten und Chunks")?;
|
||||
report.total_dirs = dirs;
|
||||
report.total_files = files;
|
||||
report.total_chunks = chunks_count;
|
||||
|
||||
let all_nodes = db.list_all_nodes().context("Fehler beim Laden der Knotenliste")?;
|
||||
report.total_nodes = all_nodes.len();
|
||||
|
||||
let mut node_map = HashMap::new();
|
||||
for node in &all_nodes {
|
||||
node_map.insert(node.id, node.clone());
|
||||
}
|
||||
|
||||
// Root-Knoten prüfen (id = 1 und optional id = 2 für Hidden Vault)
|
||||
match node_map.get(&1) {
|
||||
Some(root) => {
|
||||
if !root.is_dir {
|
||||
report.errors.push("Root-Knoten (id=1) ist nicht als Verzeichnis markiert!".to_string());
|
||||
}
|
||||
if root.parent_id.is_some() {
|
||||
report.errors.push("Root-Knoten (id=1) darf keinen Parent haben!".to_string());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
report.errors.push("Root-Knoten (id=1) fehlt in der nodes-Tabelle!".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(root2) = node_map.get(&2) {
|
||||
if !root2.is_dir {
|
||||
report.errors.push("Root-Knoten (id=2) ist nicht als Verzeichnis markiert!".to_string());
|
||||
}
|
||||
if root2.parent_id.is_some() {
|
||||
report.errors.push("Root-Knoten (id=2) darf keinen Parent haben!".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Alle anderen Knoten prüfen: Existenz des Parents, keine Zyklen
|
||||
for node in &all_nodes {
|
||||
if node.id == 1 || node.id == 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
match node.parent_id {
|
||||
Some(pid) => match node_map.get(&pid) {
|
||||
Some(parent) => {
|
||||
if !parent.is_dir {
|
||||
report.errors.push(format!(
|
||||
"Knoten '{}' (id={}) hat einen Parent (id={}), der kein Verzeichnis ist!",
|
||||
node.name, node.id, pid
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
report.orphan_nodes += 1;
|
||||
report.errors.push(format!(
|
||||
"Verwaister Knoten: '{}' (id={}) verweist auf nicht-existenten Parent id={}",
|
||||
node.name, node.id, pid
|
||||
));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
report.orphan_nodes += 1;
|
||||
report.errors.push(format!(
|
||||
"Verwaister Knoten ohne Parent: '{}' (id={})",
|
||||
node.name, node.id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Zyklenprüfung
|
||||
let mut visited = HashSet::new();
|
||||
visited.insert(node.id);
|
||||
let mut curr_parent = node.parent_id;
|
||||
while let Some(pid) = curr_parent {
|
||||
if !visited.insert(pid) {
|
||||
report.errors.push(format!(
|
||||
"Zyklische Verzeichnisreferenz bei Knoten '{}' (id={}) entdeckt!",
|
||||
node.name, node.id
|
||||
));
|
||||
break;
|
||||
}
|
||||
curr_parent = node_map.get(&pid).and_then(|n| n.parent_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Ermittle die Abstammung aller Knoten zu Root 1 (Vault 0) bzw. Root 2 (Vault 1)
|
||||
let mut vault0_nodes = HashSet::new();
|
||||
let mut vault1_nodes = HashSet::new();
|
||||
vault0_nodes.insert(1i64);
|
||||
if node_map.contains_key(&2) {
|
||||
vault1_nodes.insert(2i64);
|
||||
}
|
||||
|
||||
let mut changed = true;
|
||||
while changed {
|
||||
changed = false;
|
||||
for node in &all_nodes {
|
||||
if let Some(pid) = node.parent_id {
|
||||
if vault0_nodes.contains(&pid) && !vault0_nodes.contains(&node.id) {
|
||||
vault0_nodes.insert(node.id);
|
||||
changed = true;
|
||||
} else if vault1_nodes.contains(&pid) && !vault1_nodes.contains(&node.id) {
|
||||
vault1_nodes.insert(node.id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Falls ein DEK übergeben wurde: Bestimme, zu welchem Vault er gehört
|
||||
let active_vault_nodes = if let Some(active_dek) = dek {
|
||||
let is_vault1 = {
|
||||
let mut found_v1 = false;
|
||||
for node in &all_nodes {
|
||||
if node.parent_id == Some(2) {
|
||||
if crate::crypto::decrypt_node_name(active_dek, 2, &node.name).is_some() {
|
||||
found_v1 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
found_v1
|
||||
};
|
||||
|
||||
if is_vault1 {
|
||||
Some(&vault1_nodes)
|
||||
} else {
|
||||
Some(&vault0_nodes)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 4. Kryptografische Chunk- & AEAD-Authentifizierungsprüfung
|
||||
let chunk_headers = db.list_all_chunk_headers()
|
||||
.context("Fehler beim Abrufen der Chunk-Liste")?;
|
||||
|
||||
let format_version = meta.as_ref().map(|m| m.version).unwrap_or(FORMAT_VERSION_V2);
|
||||
|
||||
for (node_id, chunk_index) in chunk_headers {
|
||||
if !node_map.contains_key(&node_id) {
|
||||
report.errors.push(format!(
|
||||
"Verwaister Daten-Chunk: Node #{node_id} Chunk #{chunk_index} gehört zu keinem bekannten Inode!"
|
||||
));
|
||||
}
|
||||
|
||||
if let (Some(active_dek), Some(target_nodes)) = (dek, active_vault_nodes) {
|
||||
// Nur Chunks verifizieren, die zum verifizierten Tresor gehören (kein Falschalarm für Hidden Vault)
|
||||
if target_nodes.contains(&node_id) && full_chunks {
|
||||
match db.read_chunk(node_id, chunk_index) {
|
||||
Ok(Some(record)) => {
|
||||
match decrypt_chunk(
|
||||
active_dek,
|
||||
node_id,
|
||||
chunk_index,
|
||||
&record.ciphertext,
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
format_version,
|
||||
) {
|
||||
Ok(plaintext) => {
|
||||
report.total_bytes_decrypted += plaintext.len() as u64;
|
||||
}
|
||||
Err(e) => {
|
||||
report.corrupted_chunks += 1;
|
||||
report.errors.push(format!(
|
||||
"AEAD/Integritätsfehler bei Node #{node_id} Chunk #{chunk_index}: {e}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
report.errors.push(format!(
|
||||
"Chunk #{chunk_index} für Node #{node_id} in Index gefunden, aber Daten nicht lesbar!"
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
report.corrupted_chunks += 1;
|
||||
report.errors.push(format!(
|
||||
"DB-Lesefehler bei Node #{node_id} Chunk #{chunk_index}: {e}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::{
|
||||
derive_kek, encrypt_chunk, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_verify_healthy_container() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf =
|
||||
temp_dir.join(format!("test_verify_ok_{}.sanctum", std::process::id()));
|
||||
|
||||
if container_path.exists() {
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let password = "HealthyTestPassword123!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
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();
|
||||
|
||||
// Verzeichnis & Datei anlegen
|
||||
let folder = db.create_node(1, "photos", true).unwrap();
|
||||
let file = db.create_node(folder.id, "img.jpg", false).unwrap();
|
||||
|
||||
// 2 Chunks schreiben
|
||||
let chunk0_data = b"Sample JPEG data header and pixels";
|
||||
let (ct0, n0, t0) = encrypt_chunk(&dek, file.id, 0, chunk0_data, FORMAT_VERSION).unwrap();
|
||||
db.write_chunk(file.id, 0, &n0, &t0, &ct0).unwrap();
|
||||
|
||||
let chunk1_data = b"Additional payload data bytes";
|
||||
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION).unwrap();
|
||||
db.write_chunk(file.id, 1, &n1, &t1, &ct1).unwrap();
|
||||
|
||||
db.update_node_size_and_time(file.id, (chunk0_data.len() + chunk1_data.len()) as u64, 1000).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Verifizieren
|
||||
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
|
||||
assert!(report.is_healthy(), "Container must be healthy, report: {:?}", report);
|
||||
assert_eq!(report.total_files, 1);
|
||||
assert_eq!(report.total_dirs, 3); // Root 1 + Root 2 (Plausible Deniability) + photos
|
||||
assert_eq!(report.total_chunks, 2);
|
||||
assert_eq!(report.corrupted_chunks, 0);
|
||||
assert_eq!(report.orphan_nodes, 0);
|
||||
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_detects_bitrot() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf =
|
||||
temp_dir.join(format!("test_verify_bitrot_{}.sanctum", std::process::id()));
|
||||
|
||||
if container_path.exists() {
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let password = "BitrotTestPassword123!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
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();
|
||||
|
||||
let file = db.create_node(1, "document.pdf", false).unwrap();
|
||||
let chunk_data = b"Vital documents that must not be corrupted";
|
||||
let (ct, n, t) = encrypt_chunk(&dek, file.id, 0, chunk_data, FORMAT_VERSION).unwrap();
|
||||
db.write_chunk(file.id, 0, &n, &t, &ct).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
drop(db);
|
||||
|
||||
// Bitrot simulieren: Wir flippen 1 Byte im Ciphertext in SQLite direkt
|
||||
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||
let mut corrupted_ct = ct.clone();
|
||||
corrupted_ct[4] ^= 0xFF; // Bit-Flip!
|
||||
conn.execute(
|
||||
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = 0",
|
||||
rusqlite::params![corrupted_ct, file.id],
|
||||
).unwrap();
|
||||
drop(conn);
|
||||
|
||||
// Verifizieren: Muss Bitrot via AEAD Tag-Fehler sofort entlarven!
|
||||
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
|
||||
assert!(!report.is_healthy(), "Container must report unhealthy due to bitrot");
|
||||
assert_eq!(report.corrupted_chunks, 1, "Must detect exactly 1 corrupted chunk");
|
||||
assert!(report.errors.iter().any(|e| e.contains("AEAD/Integritätsfehler")));
|
||||
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
}
|
||||
+688
-105
File diff suppressed because it is too large
Load Diff
+720
@@ -0,0 +1,720 @@
|
||||
use std::process::Command;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
|
||||
pub fn find_next_available_drive() -> Result<char> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn GetLogicalDrives() -> u32;
|
||||
}
|
||||
|
||||
let mask = unsafe { GetLogicalDrives() };
|
||||
if mask == 0 {
|
||||
bail!("Fehler beim Abfragen der logischen Laufwerke via Win32 API");
|
||||
}
|
||||
|
||||
// Suche von 'Z' abwärts bis 'D' (A, B für Diskettenlaufwerke, C für System reservieren)
|
||||
for ch in ('D'..='Z').rev() {
|
||||
let bit_index = (ch as u8) - b'A';
|
||||
if (mask & (1 << bit_index)) == 0 {
|
||||
return Ok(ch);
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Kein freier Windows-Laufwerksbuchstabe (D: bis Z:) verfügbar!");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok('S')
|
||||
}
|
||||
}
|
||||
|
||||
/// Ö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<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
|
||||
Command::new("explorer.exe")
|
||||
.arg(&drive_path)
|
||||
.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).
|
||||
pub fn notify_shell_associations_changed() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn SHChangeNotify(
|
||||
w_event_id: i32,
|
||||
u_flags: u32,
|
||||
dw_item1: *const std::ffi::c_void,
|
||||
dw_item2: *const std::ffi::c_void,
|
||||
);
|
||||
}
|
||||
|
||||
const SHCNE_ASSOCCHANGED: i32 = 0x0800_0000;
|
||||
const SHCNF_IDLIST: u32 = 0x0000;
|
||||
|
||||
unsafe {
|
||||
SHChangeNotify(
|
||||
SHCNE_ASSOCCHANGED,
|
||||
SHCNF_IDLIST,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
}
|
||||
#[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 bzw. unter Linux via Freedesktop.
|
||||
pub fn register_explorer_integration() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
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 = [
|
||||
// 1. .sanctum Erweiterung mit ProgID verknüpfen
|
||||
(
|
||||
r"HKCU\Software\Classes\.sanctum",
|
||||
"",
|
||||
"Sanctum.Container",
|
||||
),
|
||||
// 2. ProgID Metadaten & Beschreibung
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container",
|
||||
"",
|
||||
"Sanctum Verschlüsselter Container",
|
||||
),
|
||||
// 3. Icon
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon",
|
||||
"",
|
||||
&format!("\"{exe_str}\",0"),
|
||||
),
|
||||
// 4. Standard-Doppelklick-Aktion: Mount
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open",
|
||||
"",
|
||||
"In Sanctum öffnen",
|
||||
),
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
|
||||
"",
|
||||
&format!("\"{exe_str}\" mount \"%1\""),
|
||||
),
|
||||
// 5. Kontextmenü-Aktion: Verify / FSCK
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
|
||||
"",
|
||||
"Integrität prüfen (FSCK)",
|
||||
),
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
|
||||
"",
|
||||
&format!("cmd /k \"\"{exe_str}\" verify \"%1\"\""),
|
||||
),
|
||||
// 6. Kontextmenü-Aktion: Header-Backup
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup",
|
||||
"",
|
||||
"Header sichern (Disaster Recovery)",
|
||||
),
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup\command",
|
||||
"",
|
||||
&format!("cmd /k \"\"{exe_str}\" backup-header \"%1\"\""),
|
||||
),
|
||||
];
|
||||
|
||||
for (key, val_name, val_data) in reg_commands {
|
||||
let mut cmd = Command::new("reg");
|
||||
cmd.arg("add").arg(key);
|
||||
if val_name.is_empty() {
|
||||
cmd.arg("/ve");
|
||||
} else {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
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";
|
||||
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) bzw. unter Linux aus Freedesktop.
|
||||
pub fn unregister_explorer_integration() -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let keys_to_delete = [
|
||||
r"HKCU\Software\Classes\.sanctum",
|
||||
r"HKCU\Software\Classes\Sanctum.Container",
|
||||
];
|
||||
|
||||
for key in keys_to_delete {
|
||||
let _ = Command::new("reg")
|
||||
.args(["delete", key, "/f"])
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
|
||||
#[cfg(windows)]
|
||||
pub fn get_default_system_icon() -> Option<tray_item::IconSource> {
|
||||
extern "system" {
|
||||
fn LoadIconW(instance: isize, icon_name: *const u16) -> isize;
|
||||
fn GetModuleHandleW(module_name: *const u16) -> isize;
|
||||
}
|
||||
|
||||
// 1. Eingebettetes Anwendungs-Icon (Ressource ID 1) aus eigenem Modul laden
|
||||
let h_instance = unsafe { GetModuleHandleW(std::ptr::null()) };
|
||||
let app_icon = unsafe { LoadIconW(h_instance, 1 as *const u16) };
|
||||
if app_icon != 0 {
|
||||
return Some(tray_item::IconSource::RawIcon(app_icon));
|
||||
}
|
||||
|
||||
// 2. Fallback: Windows IDI_SHIELD = 32518, IDI_APPLICATION = 32512
|
||||
let icon = unsafe { LoadIconW(0, 32518 as *const u16) };
|
||||
if icon != 0 {
|
||||
Some(tray_item::IconSource::RawIcon(icon))
|
||||
} else {
|
||||
let icon_app = unsafe { LoadIconW(0, 32512 as *const u16) };
|
||||
if icon_app != 0 {
|
||||
Some(tray_item::IconSource::RawIcon(icon_app))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre.
|
||||
/// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet.
|
||||
#[cfg(windows)]
|
||||
pub struct SessionLockGuard {
|
||||
hwnd: isize,
|
||||
join_handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl Drop for SessionLockGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.hwnd != 0 {
|
||||
unsafe {
|
||||
PostMessageW(self.hwnd, 0x0010 /* WM_CLOSE */, 0, 0);
|
||||
}
|
||||
}
|
||||
if let Ok(mut guard) = SESSION_LOCK_TX.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
if let Some(handle) = self.join_handle.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub struct SessionLockGuard;
|
||||
|
||||
#[cfg(windows)]
|
||||
static SESSION_LOCK_TX: std::sync::Mutex<Option<tokio::sync::mpsc::Sender<()>>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
#[cfg(windows)]
|
||||
#[allow(non_snake_case)]
|
||||
#[repr(C)]
|
||||
struct WNDCLASSEXW {
|
||||
cbSize: u32,
|
||||
style: u32,
|
||||
lpfnWndProc: Option<unsafe extern "system" fn(isize, u32, usize, isize) -> isize>,
|
||||
cbClsExtra: i32,
|
||||
cbWndExtra: i32,
|
||||
hInstance: isize,
|
||||
hIcon: isize,
|
||||
hCursor: isize,
|
||||
hbrBackground: isize,
|
||||
lpszMenuName: *const u16,
|
||||
lpszClassName: *const u16,
|
||||
hIconSm: isize,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[allow(non_snake_case)]
|
||||
#[repr(C)]
|
||||
struct MSG {
|
||||
hwnd: isize,
|
||||
message: u32,
|
||||
wParam: usize,
|
||||
lParam: isize,
|
||||
time: u32,
|
||||
pt_x: i32,
|
||||
pt_y: i32,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
extern "system" {
|
||||
fn GetModuleHandleW(lpModuleName: *const u16) -> isize;
|
||||
fn RegisterClassExW(lpwcx: *const WNDCLASSEXW) -> u16;
|
||||
fn UnregisterClassW(lpClassName: *const u16, hInstance: isize) -> i32;
|
||||
fn CreateWindowExW(
|
||||
dwExStyle: u32,
|
||||
lpClassName: *const u16,
|
||||
lpWindowName: *const u16,
|
||||
dwStyle: u32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
nWidth: i32,
|
||||
nHeight: i32,
|
||||
hWndParent: isize,
|
||||
hMenu: isize,
|
||||
hInstance: isize,
|
||||
lpParam: *mut std::ffi::c_void,
|
||||
) -> isize;
|
||||
fn DestroyWindow(hwnd: isize) -> i32;
|
||||
fn DefWindowProcW(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> isize;
|
||||
fn GetMessageW(lpMsg: *mut MSG, hWnd: isize, wMsgFilterMin: u32, wMsgFilterMax: u32) -> i32;
|
||||
fn TranslateMessage(lpMsg: *const MSG) -> i32;
|
||||
fn DispatchMessageW(lpMsg: *const MSG) -> isize;
|
||||
fn PostMessageW(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> i32;
|
||||
fn PostQuitMessage(nExitCode: i32);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[link(name = "wtsapi32")]
|
||||
extern "system" {
|
||||
fn WTSRegisterSessionNotification(hwnd: isize, flags: u32) -> i32;
|
||||
fn WTSUnRegisterSessionNotification(hwnd: isize) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe extern "system" fn session_wnd_proc(
|
||||
hwnd: isize,
|
||||
msg: u32,
|
||||
wparam: usize,
|
||||
lparam: isize,
|
||||
) -> isize {
|
||||
const WM_CLOSE: u32 = 0x0010;
|
||||
const WM_WTSSESSION_CHANGE: u32 = 0x02B1;
|
||||
const WTS_SESSION_LOCK: usize = 0x7;
|
||||
const WTS_SESSION_LOGOFF: usize = 0x6;
|
||||
|
||||
match msg {
|
||||
WM_WTSSESSION_CHANGE => {
|
||||
if wparam == WTS_SESSION_LOCK || wparam == WTS_SESSION_LOGOFF {
|
||||
if let Ok(guard) = SESSION_LOCK_TX.lock() {
|
||||
if let Some(ref tx) = *guard {
|
||||
let _ = tx.blocking_send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
WM_CLOSE => {
|
||||
PostQuitMessage(0);
|
||||
0
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
|
||||
/// Startet einen Hintergrundthread mit einem verdeckten Win32-Nachrichtenfenster (HWND_MESSAGE),
|
||||
/// das via `WTSRegisterSessionNotification` auf Sperr-Events (Win + L) lauscht und beim Eintreffen
|
||||
/// ein Signal an den übergebenen Tokio-Kanal sendet.
|
||||
#[cfg(windows)]
|
||||
pub fn start_session_lock_monitor(
|
||||
shutdown_tx: tokio::sync::mpsc::Sender<()>,
|
||||
) -> Result<SessionLockGuard> {
|
||||
if let Ok(mut guard) = SESSION_LOCK_TX.lock() {
|
||||
*guard = Some(shutdown_tx);
|
||||
}
|
||||
|
||||
let (hwnd_tx, hwnd_rx) = std::sync::mpsc::channel::<isize>();
|
||||
|
||||
let join_handle = std::thread::Builder::new()
|
||||
.name("sanctum-session-monitor".to_string())
|
||||
.spawn(move || unsafe {
|
||||
let h_instance = GetModuleHandleW(std::ptr::null());
|
||||
let class_name: Vec<u16> = "SanctumSessionMonitorClass\0".encode_utf16().collect();
|
||||
|
||||
let wcx = WNDCLASSEXW {
|
||||
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
|
||||
style: 0,
|
||||
lpfnWndProc: Some(session_wnd_proc),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
hInstance: h_instance,
|
||||
hIcon: 0,
|
||||
hCursor: 0,
|
||||
hbrBackground: 0,
|
||||
lpszMenuName: std::ptr::null(),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
hIconSm: 0,
|
||||
};
|
||||
|
||||
RegisterClassExW(&wcx);
|
||||
|
||||
let hwnd = CreateWindowExW(
|
||||
0,
|
||||
class_name.as_ptr(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-3, // HWND_MESSAGE
|
||||
0,
|
||||
h_instance,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
|
||||
if hwnd == 0 {
|
||||
let _ = hwnd_tx.send(0);
|
||||
return;
|
||||
}
|
||||
|
||||
WTSRegisterSessionNotification(hwnd, 0); // NOTIFY_FOR_THIS_SESSION = 0
|
||||
|
||||
let _ = hwnd_tx.send(hwnd);
|
||||
|
||||
let mut msg = std::mem::zeroed::<MSG>();
|
||||
while GetMessageW(&mut msg, 0, 0, 0) > 0 {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
WTSUnRegisterSessionNotification(hwnd);
|
||||
DestroyWindow(hwnd);
|
||||
UnregisterClassW(class_name.as_ptr(), h_instance);
|
||||
})
|
||||
.context("Konnte Windows Session-Monitor-Thread nicht starten")?;
|
||||
|
||||
let hwnd = hwnd_rx
|
||||
.recv()
|
||||
.map_err(|e| anyhow::anyhow!("Session-Monitor-Thread initialisierte nicht rechtzeitig: {e}"))?;
|
||||
|
||||
if hwnd == 0 {
|
||||
bail!("Win32-Nachrichtenfenster für Session-Lock konnte nicht erstellt werden");
|
||||
}
|
||||
|
||||
Ok(SessionLockGuard {
|
||||
hwnd,
|
||||
join_handle: Some(join_handle),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn start_session_lock_monitor(
|
||||
_shutdown_tx: tokio::sync::mpsc::Sender<()>,
|
||||
) -> Result<SessionLockGuard> {
|
||||
Ok(SessionLockGuard)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Console Close Event Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(windows)]
|
||||
static CONSOLE_CTRL_TX: std::sync::Mutex<Option<tokio::sync::mpsc::Sender<()>>> = std::sync::Mutex::new(None);
|
||||
#[cfg(windows)]
|
||||
static CONSOLE_CTRL_DRIVE: std::sync::Mutex<Option<char>> = std::sync::Mutex::new(None);
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe extern "system" fn console_ctrl_routine(ctrl_type: u32) -> i32 {
|
||||
const CTRL_C_EVENT: u32 = 0;
|
||||
const CTRL_BREAK_EVENT: u32 = 1;
|
||||
const CTRL_CLOSE_EVENT: u32 = 2;
|
||||
const CTRL_LOGOFF_EVENT: u32 = 5;
|
||||
const CTRL_SHUTDOWN_EVENT: u32 = 6;
|
||||
|
||||
match ctrl_type {
|
||||
CTRL_CLOSE_EVENT | CTRL_LOGOFF_EVENT | CTRL_SHUTDOWN_EVENT => {
|
||||
// 1. Sofortiges Notfall-Unmount direkt aus dem Win32-Callback ausführen
|
||||
if let Ok(guard) = CONSOLE_CTRL_DRIVE.lock() {
|
||||
if let Some(dl) = *guard {
|
||||
let drive_str = format!("{}:", dl.to_ascii_uppercase());
|
||||
let _ = std::process::Command::new("net")
|
||||
.args(["use", &drive_str, "/delete", "/y"])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
// 2. Asynchronen Shutdown-Kanal benachrichtigen (für SQLite WAL Checkpoint)
|
||||
if let Ok(guard) = CONSOLE_CTRL_TX.lock() {
|
||||
if let Some(ref tx) = *guard {
|
||||
let _ = tx.blocking_send(());
|
||||
}
|
||||
}
|
||||
1 // TRUE: Event abgearbeitet
|
||||
}
|
||||
CTRL_C_EVENT | CTRL_BREAK_EVENT => {
|
||||
// Ctrl+C wird primär von tokio::signal::ctrl_c() behandelt
|
||||
0
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII Guard zur sauberen Deregistrierung des Win32 Console-Control-Handlers.
|
||||
pub struct ConsoleCtrlGuard;
|
||||
|
||||
impl Drop for ConsoleCtrlGuard {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn SetConsoleCtrlHandler(
|
||||
handler: Option<unsafe extern "system" fn(u32) -> i32>,
|
||||
add: i32,
|
||||
) -> i32;
|
||||
}
|
||||
unsafe {
|
||||
SetConsoleCtrlHandler(Some(console_ctrl_routine), 0);
|
||||
}
|
||||
if let Ok(mut guard) = CONSOLE_CTRL_TX.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
if let Ok(mut guard) = CONSOLE_CTRL_DRIVE.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registriert einen Win32 Console Control Handler für CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT
|
||||
/// und CTRL_SHUTDOWN_EVENT, um verwaiste Netzlaufwerke beim Schließen des Konsolenfensters zu verhindern.
|
||||
pub fn start_console_ctrl_monitor(
|
||||
shutdown_tx: tokio::sync::mpsc::Sender<()>,
|
||||
drive_letter: char,
|
||||
) -> Result<ConsoleCtrlGuard> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn SetConsoleCtrlHandler(
|
||||
handler: Option<unsafe extern "system" fn(u32) -> i32>,
|
||||
add: i32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
if let Ok(mut guard) = CONSOLE_CTRL_TX.lock() {
|
||||
*guard = Some(shutdown_tx);
|
||||
}
|
||||
if let Ok(mut guard) = CONSOLE_CTRL_DRIVE.lock() {
|
||||
*guard = Some(drive_letter);
|
||||
}
|
||||
|
||||
let res = unsafe { SetConsoleCtrlHandler(Some(console_ctrl_routine), 1) };
|
||||
if res == 0 {
|
||||
bail!("Konnte Win32 SetConsoleCtrlHandler nicht registrieren");
|
||||
}
|
||||
|
||||
Ok(ConsoleCtrlGuard)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = shutdown_tx;
|
||||
let _ = drive_letter;
|
||||
Ok(ConsoleCtrlGuard)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn VirtualLock(lpaddress: *const std::ffi::c_void, dwsize: usize) -> i32;
|
||||
}
|
||||
if ptr.is_null() || len == 0 {
|
||||
return false;
|
||||
}
|
||||
unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 }
|
||||
}
|
||||
#[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);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Entriegelt einen zuvor mit `lock_memory` geschützten Speicherbereich im RAM.
|
||||
pub fn unlock_memory(ptr: *const u8, len: usize) -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn VirtualUnlock(lpaddress: *const std::ffi::c_void, dwsize: usize) -> i32;
|
||||
}
|
||||
if ptr.is_null() || len == 0 {
|
||||
return false;
|
||||
}
|
||||
unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 }
|
||||
}
|
||||
#[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);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_virtual_lock_memory_lifecycle() {
|
||||
let buffer = vec![0x42u8; 4096];
|
||||
let _ = lock_memory(buffer.as_ptr(), buffer.len());
|
||||
let _ = unlock_memory(buffer.as_ptr(), buffer.len());
|
||||
assert_eq!(buffer[0], 0x42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_next_available_drive() {
|
||||
let drive = find_next_available_drive().expect("Find next drive");
|
||||
assert!(drive.is_ascii_alphabetic());
|
||||
assert!(drive >= 'D' && drive <= 'Z');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_lock_monitor_lifecycle() {
|
||||
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
||||
let monitor = start_session_lock_monitor(tx);
|
||||
assert!(monitor.is_ok());
|
||||
// Dropping monitor closes message loop and joins thread cleanly
|
||||
drop(monitor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_console_ctrl_monitor_lifecycle() {
|
||||
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
||||
let monitor = start_console_ctrl_monitor(tx, 'Z');
|
||||
assert!(monitor.is_ok());
|
||||
drop(monitor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bytes::Bytes;
|
||||
use dav_server::davpath::DavPath;
|
||||
use dav_server::fs::{DavFileSystem, FsError, OpenOptions, ReadDirMeta};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, CHUNK_SIZE,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::verify::verify_container;
|
||||
use sanctum::vfs::SanctumFs;
|
||||
|
||||
fn temp_db_path(prefix: &str) -> PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
let id: u64 = OsRng.next_u64();
|
||||
path.push(format!("sanctum_test_{}_{}.sanctum", prefix, id));
|
||||
path
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
||||
let path = temp_db_path("carrier_accounting");
|
||||
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB (10 Blöcke à 1 MB)
|
||||
let carrier_name = "system_backup.dat";
|
||||
|
||||
let pass_decoy = "DecoyPassword2026!";
|
||||
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let salt_0 = generate_salt();
|
||||
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_0, nonce_0, tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
let (wrapped_1, nonce_1, tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
|
||||
let db = Database::open(&path).expect("Open database");
|
||||
|
||||
let created_cid = db
|
||||
.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params,
|
||||
&wrapped_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((
|
||||
carrier_name,
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params,
|
||||
&wrapped_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
)
|
||||
.expect("Init carrier schema");
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
assert_eq!(created_cid, Some(carrier_node_id));
|
||||
|
||||
// 1. Authentifizierung beider Passwörter
|
||||
let meta = db.read_meta().unwrap();
|
||||
let auth_decoy = meta.authenticate(pass_decoy).expect("Auth decoy");
|
||||
assert_eq!(auth_decoy.slot_id(), 0);
|
||||
assert_eq!(**auth_decoy.dek(), *dek_0);
|
||||
assert_eq!(auth_decoy.carrier_node_id(), Some(carrier_node_id));
|
||||
|
||||
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||
assert_eq!(auth_hidden.slot_id(), 1);
|
||||
assert_eq!(**auth_hidden.dek(), *dek_1);
|
||||
assert_eq!(*auth_hidden.carrier_dek().unwrap(), *dek_0);
|
||||
assert_eq!(auth_hidden.carrier_node_id(), Some(carrier_node_id));
|
||||
|
||||
// 2. Decoy Mount: Schutz der Alibi-Datei (system_backup.dat)
|
||||
let decoy_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_decoy.dek().clone(),
|
||||
auth_decoy.carrier_dek(),
|
||||
auth_decoy.carrier_node_id(),
|
||||
auth_decoy.version(),
|
||||
true,
|
||||
0,
|
||||
);
|
||||
|
||||
let carrier_path = DavPath::new("/system_backup.dat").unwrap();
|
||||
|
||||
// Metadaten der Alibi-Datei im Decoy prüfen
|
||||
let carrier_meta = decoy_fs.metadata(&carrier_path).await.expect("Carrier meta");
|
||||
assert_eq!(carrier_meta.len(), carrier_size_bytes);
|
||||
assert!(!carrier_meta.is_dir());
|
||||
|
||||
// Alibi-Datei darf im Decoy-Mount NICHT zum Schreiben geöffnet werden
|
||||
let write_opts = OpenOptions {
|
||||
write: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
matches!(decoy_fs.open(&carrier_path, write_opts).await, Err(FsError::Forbidden)),
|
||||
"Alibi-Datei darf nicht zum Schreiben geöffnet werden"
|
||||
);
|
||||
|
||||
// Alibi-Datei darf im Decoy-Mount NICHT gelöscht werden
|
||||
assert!(
|
||||
matches!(decoy_fs.remove_file(&carrier_path).await, Err(FsError::Forbidden)),
|
||||
"Alibi-Datei darf nicht gelöscht werden"
|
||||
);
|
||||
|
||||
// Alibi-Datei darf im Decoy-Mount NICHT umbenannt werden
|
||||
let new_name = DavPath::new("/renamed.iso").unwrap();
|
||||
assert!(
|
||||
matches!(decoy_fs.rename(&carrier_path, &new_name).await, Err(FsError::Forbidden)),
|
||||
"Alibi-Datei darf nicht umbenannt werden"
|
||||
);
|
||||
|
||||
// Alibi-Datei KANN im Decoy-Mount gelesen werden
|
||||
let read_opts = OpenOptions {
|
||||
read: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut file_reader = decoy_fs.open(&carrier_path, read_opts).await.expect("Open read");
|
||||
let first_mb = file_reader.read_bytes(CHUNK_SIZE).await.expect("Read first chunk");
|
||||
assert_eq!(first_mb.len(), CHUNK_SIZE);
|
||||
|
||||
// 3. Hidden Mount: Dateisystem-Operationen innerhalb des Alibi-Carriers
|
||||
let hidden_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_hidden.dek().clone(),
|
||||
auth_hidden.carrier_dek(),
|
||||
auth_hidden.carrier_node_id(),
|
||||
auth_hidden.version(),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
// Wurzelverzeichnis des Hidden Vault auflisten (anfangs leer)
|
||||
let root_path = DavPath::new("/").unwrap();
|
||||
let mut stream = hidden_fs
|
||||
.read_dir(&root_path, ReadDirMeta::None)
|
||||
.await
|
||||
.expect("Read dir root");
|
||||
use futures_util::StreamExt;
|
||||
let mut entries = Vec::new();
|
||||
while let Some(item) = stream.next().await {
|
||||
entries.push(item.unwrap().name());
|
||||
}
|
||||
assert!(entries.is_empty(), "Hidden Vault Wurzelverzeichnis muss anfangs leer sein");
|
||||
|
||||
// Ordner erstellen
|
||||
let secret_dir = DavPath::new("/Classified").unwrap();
|
||||
hidden_fs.create_dir(&secret_dir).await.expect("Create Classified dir");
|
||||
|
||||
// Datei im Ordner anlegen und schreiben
|
||||
let secret_file_path = DavPath::new("/Classified/passwords.txt").unwrap();
|
||||
let create_opts = OpenOptions {
|
||||
create: true,
|
||||
write: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut secret_file = hidden_fs
|
||||
.open(&secret_file_path, create_opts)
|
||||
.await
|
||||
.expect("Create secret file");
|
||||
|
||||
let secret_content = b"TopSecretCredentials_2026_SanctumCoreSecureVault";
|
||||
secret_file
|
||||
.write_bytes(Bytes::from_static(secret_content))
|
||||
.await
|
||||
.expect("Write secret content");
|
||||
secret_file.flush().await.expect("Flush secret file");
|
||||
drop(secret_file);
|
||||
|
||||
// Datei lesen und verifizieren
|
||||
let read_opts = OpenOptions {
|
||||
read: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut read_handle = hidden_fs
|
||||
.open(&secret_file_path, read_opts)
|
||||
.await
|
||||
.expect("Open secret file for read");
|
||||
let read_data = read_handle.read_bytes(1024).await.expect("Read secret bytes");
|
||||
assert_eq!(&read_data[..], secret_content);
|
||||
drop(read_handle);
|
||||
|
||||
// Größere Binärdatei schreiben (über 2 MB = 2 Blöcke)
|
||||
let big_file_path = DavPath::new("/Classified/payload.bin").unwrap();
|
||||
let mut big_file = hidden_fs
|
||||
.open(&big_file_path, OpenOptions { create: true, write: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Create big file");
|
||||
|
||||
let payload_size = 2 * 1024 * 1024 + 12345; // 2 MB + 12.345 Bytes
|
||||
let mut payload = vec![0u8; payload_size];
|
||||
OsRng.fill_bytes(&mut payload);
|
||||
|
||||
big_file.write_bytes(Bytes::copy_from_slice(&payload)).await.expect("Write big payload");
|
||||
big_file.flush().await.expect("Flush big file");
|
||||
drop(big_file);
|
||||
|
||||
// Datei zurücklesen und Bit-für-Bit verifizieren
|
||||
let mut read_big = hidden_fs
|
||||
.open(&big_file_path, OpenOptions { read: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open big file");
|
||||
let read_big_bytes = read_big.read_bytes(payload_size + 100).await.expect("Read big file bytes");
|
||||
assert_eq!(read_big_bytes.len(), payload_size);
|
||||
assert_eq!(&read_big_bytes[..], &payload[..]);
|
||||
drop(read_big);
|
||||
|
||||
// Datei umbenennen
|
||||
let renamed_path = DavPath::new("/Classified/renamed_payload.bin").unwrap();
|
||||
hidden_fs.rename(&big_file_path, &renamed_path).await.expect("Rename file");
|
||||
assert!(hidden_fs.metadata(&big_file_path).await.is_err());
|
||||
assert!(hidden_fs.metadata(&renamed_path).await.is_ok());
|
||||
|
||||
// Datei löschen (Blöcke werden geshreddert und freigegeben)
|
||||
hidden_fs.remove_file(&renamed_path).await.expect("Remove file");
|
||||
assert!(hidden_fs.metadata(&renamed_path).await.is_err());
|
||||
|
||||
// Checkpoint SQLite
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// 4. CHUNKS-ACCOUNTING-ANGRIFF & INTEGRITÄTSPRÜFUNG
|
||||
// Ein Angreifer besitzt nur das Decoy-Passwort (dek_0).
|
||||
// Er führt eine 100%-ige kryptografische AEAD-Prüfung aller Chunks in der SQLite-Datenbank durch.
|
||||
// ALLE Chunks müssen fehlerfrei unter DEK_0 entschlüsseln!
|
||||
let report = verify_container(&path, Some(&dek_0), true).expect("Verify with DEK_0");
|
||||
assert!(
|
||||
report.is_healthy(),
|
||||
"Container muss für einen Angreifer mit DEK_0 100% gesund und fehlerfrei sein! Fehler: {:?}",
|
||||
report.errors
|
||||
);
|
||||
assert_eq!(
|
||||
report.corrupted_chunks, 0,
|
||||
"Chunks-Accounting: Es darf exakt 0 korrupte Chunks unter DEK_0 geben!"
|
||||
);
|
||||
assert_eq!(
|
||||
report.orphan_nodes, 0,
|
||||
"Es darf keine verwaisten Knoten geben!"
|
||||
);
|
||||
|
||||
// Aufräumen
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_model_a_container_file_size_invariance() {
|
||||
let path = temp_db_path("carrier_size_invariance");
|
||||
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
let pass_decoy = "DecoyPass2026!";
|
||||
let pass_hidden = "HiddenPass2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let salt_0 = generate_salt();
|
||||
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_0, nonce_0, tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
let (wrapped_1, nonce_1, tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
|
||||
let db = Database::open(&path).expect("Open database");
|
||||
|
||||
db.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params,
|
||||
&wrapped_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((
|
||||
"virtual_disk.vhdx",
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params,
|
||||
&wrapped_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
)
|
||||
.expect("Init carrier schema");
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Initiale Dateigröße messen
|
||||
let initial_file_size = std::fs::metadata(&path).unwrap().len();
|
||||
assert!(initial_file_size >= carrier_size_bytes, "Containergröße muss mindestens 10 MB betragen");
|
||||
|
||||
// Hidden Mount öffnen und 4 MB geheime Daten schreiben
|
||||
let meta = db.read_meta().unwrap();
|
||||
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||
|
||||
let hidden_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_hidden.dek().clone(),
|
||||
auth_hidden.carrier_dek(),
|
||||
auth_hidden.carrier_node_id(),
|
||||
auth_hidden.version(),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
let test_file = DavPath::new("/large_confidential.pdf").unwrap();
|
||||
let mut handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { create: true, write: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file");
|
||||
|
||||
let mut random_data = vec![0u8; 4 * 1024 * 1024]; // 4 MB
|
||||
OsRng.fill_bytes(&mut random_data);
|
||||
handle.write_bytes(Bytes::copy_from_slice(&random_data)).await.expect("Write 4MB");
|
||||
handle.flush().await.expect("Flush 4MB");
|
||||
drop(handle);
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Dateigröße nach dem Schreiben von 4 MB im Hidden Vault messen
|
||||
let size_after_hidden_writes = std::fs::metadata(&path).unwrap().len();
|
||||
|
||||
// Die Dateigröße auf der Festplatte DARF NICHT WACHSEN!
|
||||
// Alle Chunks wurden in vorallokierte Carrier-Blöcke überschrieben.
|
||||
assert_eq!(
|
||||
initial_file_size, size_after_hidden_writes,
|
||||
"Dateigröße auf der Festplatte darf sich beim Schreiben in den Hidden Vault NICHT verändern! Vorher: {}, Nachher: {}",
|
||||
initial_file_size, size_after_hidden_writes
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_carrier_file_drop_and_append_mode() {
|
||||
let path = temp_db_path("carrier_drop_append");
|
||||
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
|
||||
let carrier_name = "test_carrier.iso";
|
||||
|
||||
let pass_decoy = "DecoyPassword2026!";
|
||||
let pass_hidden = "HiddenSecretPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let salt_0 = generate_salt();
|
||||
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_0, nonce_0, tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
let (wrapped_1, nonce_1, tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
|
||||
let db = Database::open(&path).expect("Open database");
|
||||
|
||||
db.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params,
|
||||
&wrapped_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((
|
||||
carrier_name,
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params,
|
||||
&wrapped_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
)
|
||||
.expect("Init carrier schema");
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
let meta = db.read_meta().unwrap();
|
||||
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||
|
||||
let hidden_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_hidden.dek().clone(),
|
||||
auth_hidden.carrier_dek(),
|
||||
auth_hidden.carrier_node_id(),
|
||||
auth_hidden.version(),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
// 1. TEST CarrierFile::drop: Write bytes OHNE expliziten flush(), dann drop(handle)
|
||||
let test_file = DavPath::new("/drop_flush_test.txt").unwrap();
|
||||
let mut write_handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { create: true, write: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for write");
|
||||
|
||||
let initial_data = b"Hello from unflushed write!";
|
||||
write_handle.write_bytes(Bytes::from_static(initial_data)).await.expect("Write initial data");
|
||||
// WICHTIG: KEIN write_handle.flush()! Nur drop:
|
||||
drop(write_handle);
|
||||
|
||||
// Jetzt Datei wieder lesend öffnen und prüfen, ob Daten durch Drop persistiert wurden
|
||||
let mut read_handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { read: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for read");
|
||||
let read_back = read_handle.read_bytes(100).await.expect("Read data back");
|
||||
assert_eq!(&read_back[..], initial_data, "Drop muss ungeflushte Datenblöcke und Inode automatisch sichern");
|
||||
drop(read_handle);
|
||||
|
||||
// 2. TEST O_APPEND: Im Append-Modus öffnen und weitere Daten anhängen
|
||||
let append_data = b" - Appended data at EOF!";
|
||||
let mut append_handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { write: true, append: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for append");
|
||||
|
||||
append_handle.write_bytes(Bytes::from_static(append_data)).await.expect("Write appended data");
|
||||
drop(append_handle); // Drop sichert auch hier
|
||||
|
||||
// Prüfe den vollständigen Dateiinhalt nach Append
|
||||
let mut read_handle_2 = hidden_fs
|
||||
.open(&test_file, OpenOptions { read: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for read after append");
|
||||
let full_content = read_handle_2.read_bytes(200).await.expect("Read full content");
|
||||
let mut expected = Vec::new();
|
||||
expected.extend_from_slice(initial_data);
|
||||
expected.extend_from_slice(append_data);
|
||||
assert_eq!(&full_content[..], &expected[..], "O_APPEND muss Daten am Dateiende anhängen");
|
||||
drop(read_handle_2);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
+1011
-3
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user