Compare commits
19
Commits
99813ae2a3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80a3bd1911 | ||
|
|
2da1ea55a9 | ||
|
|
2e4cf1cef0 | ||
|
|
1048253497 | ||
|
|
aa65d96434 | ||
|
|
3f3f8f2730 | ||
|
|
650cfc7cd4 | ||
|
|
9d1a4d3ad7 | ||
|
|
aa25719c0d | ||
|
|
34378998c4 | ||
|
|
4ef0c414fa | ||
|
|
1f3afb1e52 | ||
|
|
5f2040f8bf | ||
|
|
da2ddbd6ab | ||
|
|
46f976b353 | ||
|
|
6cb48f55d9 | ||
|
|
7ed0f51fb1 | ||
|
|
dae0b3b7c3 | ||
|
|
541190cff4 |
@@ -5,6 +5,91 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
||||||
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`sanctum upgrade` (Alias: `sanctum update`) - In-Place Self-Upgrade Engine**:
|
||||||
|
- Ermöglicht das direkte Aktualisieren der laufenden Sanctum-Binary ohne externe Paketmanager oder manuelle Download-Schritte.
|
||||||
|
- **Plattformunabhängiger In-Place-Austausch**: Löst das Windows-Dateilock-Problem (`ERROR_ACCESS_DENIED`) via `self-replace` (Umbenennen der laufenden Executable und verzögerte Bereinigung). Auf Linux/Unix wird ein sauberer atomarer Tausch mit Ausführungsrechten (`0o755`) durchgeführt.
|
||||||
|
- **Kryptografische Integritätsprüfung**: Vor dem Austausch wird jedes heruntergeladene Binary zwingend gegen die offizielle `SHA256SUMS.txt` des Gitea-Releases via SHA-256 verifiziert. Bei Abweichungen bleibt die bestehende Installation vollständig unberührt.
|
||||||
|
- **Prüfmodus (`--check` / `-c`)**: Schneller Abgleich mit der Gitea-Releases-API zur Feststellung, ob ein Update vorliegt, ohne Downloads durchzuführen.
|
||||||
|
- **Automatisierungsmodus (`--yes` / `-y`)**: Führt das Upgrade ohne interaktive Bestätigung durch – ideal für geplante Aufgaben und Skripte.
|
||||||
|
- **Force-Option (`--force` / `-f`)**: Erlaubt das Erzwingen einer Neuinstallation / Aktualisierung auch bei gleicher Version oder erkannter Paketmanager-Umgebung.
|
||||||
|
- **Paketmanager-Erkennung**: Erkennt automatisch, ob Sanctum über Scoop oder Winget ausgeführt wird, und empfiehlt den entsprechenden Paketmanager-Befehl (`scoop update sanctum` bzw. `winget upgrade HaraldPansi.Sanctum`).
|
||||||
|
- **Testsuite**:
|
||||||
|
- `tests/upgrade_test.rs`: 5 neue Tests zur Verifikation von Checksum-Parsing, SemVer-Vergleich, Paketmanager-Erkennung, SHA-256 Hash-Validierung und Gitea Release API-JSON-Deserialisierung.
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`sanctum sync` - Native rsync-ähnliche Synchronisation**:
|
||||||
|
- Bidirektionale Synchronisation (`Push`: Host -> Tresor, `Pull`: Tresor -> Host) direkt auf SQLite- und VFS-Ebene unter vollständiger Umgehung des WebDAV-Layers.
|
||||||
|
- **Beseitigung des 4-GB-Dateilimits**: Löst das fundamentale Problem des Windows WebClient-Dienstes (`mrxdav.sys`), welcher systembedingt auf 32-Bit Dateigrößen (~4 GB - 1 Byte / Fehler `0x800700DF`) beschränkt ist. Sanctum streamt nun Dateien beliebiger Größe (5 GB, 50 GB, 500 GB+) in 1 MB Chunks mit AES-256-GCM direkt in/aus den Tresor.
|
||||||
|
- **Dry-Run-Simulation (`--dry-run` / `-n`)**: Ermöglicht die risikolose Vorschau aller auszuführenden Aktionen (Hinzufügen, Aktualisieren, Löschen, Überspringen) samt Datenmengen-Berechnung, ohne tatsächliche Änderungen am Ziel durchzuführen.
|
||||||
|
- **Spiegelung & Verwaiste Dateien bereinigen (`--delete`)**: Unterstützt das automatische Löschen von Dateien im Ziel, die in der Quelle nicht mehr existieren, für exakte Ordnerspiegelungen.
|
||||||
|
- **Inhaltsbasierte Hash-Prüfung (`--checksum` / `-c`)**: Optionaler byteweiser SHA-256 Inhaltsvergleich anstelle des schnellen Größen-/mtime-Prüfverfahrens.
|
||||||
|
- **Ausschlussmuster (`--exclude <PATTERN>`)**: Unterstützung für flexible Glob-Muster zum Ignorieren temporärer Dateien, Downloads oder System-Metadaten (z. B. `*.crdownload`, `*.tmp`, `Thumbs.db`).
|
||||||
|
- **Quiet-Modus (`--quiet` / `-q`)**: Ermöglicht lautlose Ausführung für Skripte und geplante Aufgaben mit knapper Zusammenfassungsstatistik.
|
||||||
|
- **Metadaten-Konservierung**: Erhaltung und Übertragung präziser Datei-Modifikationszeitstempel (`mtime`) in beide Richtungen.
|
||||||
|
- **Integrationstests**:
|
||||||
|
- `tests/sync_test.rs`: Umfassende automatisierte Testabdeckung für Push/Pull, Multi-Chunk Großdateien, Fast-Delta-Check, Dry-Run-Invarianz, `--delete` und `--exclude`.
|
||||||
|
|
||||||
|
## [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
|
## [0.3.0] - 2026-09-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+161
-1
@@ -512,6 +512,12 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -1020,6 +1026,12 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -1334,6 +1346,20 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpassword"
|
name = "rpassword"
|
||||||
version = "7.5.4"
|
version = "7.5.4"
|
||||||
@@ -1369,6 +1395,54 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -1377,7 +1451,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.3.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1396,13 +1470,17 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"self-replace",
|
||||||
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tray-item",
|
"tray-item",
|
||||||
|
"ureq",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1412,6 +1490,23 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self-replace"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"tempfile",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -1466,6 +1561,17 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -1564,6 +1670,19 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.20"
|
version = "2.0.20"
|
||||||
@@ -1780,6 +1899,29 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -1884,6 +2026,24 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.3.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
||||||
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
||||||
@@ -36,6 +36,12 @@ dyn-clone = "1.0"
|
|||||||
lz4_flex = "0.11"
|
lz4_flex = "0.11"
|
||||||
bip39 = { version = "2.2", features = ["zeroize"] }
|
bip39 = { version = "2.2", features = ["zeroize"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
self-replace = "1.5"
|
||||||
|
ureq = { version = "2.10", default-features = false, features = ["json", "tls"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
semver = "1.0"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
tray-item = "0.10"
|
tray-item = "0.10"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+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.
|
||||||
|
```
|
||||||
@@ -4,18 +4,22 @@
|
|||||||
|
|
||||||
# Sanctum 🛡️
|
# Sanctum 🛡️
|
||||||
|
|
||||||
**Verschlüsselter Ein-Datei-Container unter Windows 10/11 im reinen Userland**
|
**Verschlüsselter Ein-Datei-Container unter Windows & Linux im reinen Userland**
|
||||||
|
|
||||||
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.
|
Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (`.sanctum`) unter Windows 10/11 und Linux im reinen Userland verwaltet.
|
||||||
|
|
||||||
- **100% Userland**: Keine Administratorrechte erforderlich, keine Kernel-Treiber (weder WinFsp noch Dokan).
|
- **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.
|
- **Windows Explorer- & Desktop-Integration**: Einhängen als nativer Netzlaufwerk-Buchstabe (Z: abwärts) via lokalem WebDAV (`127.0.0.1`), Explorer-Kontextmenü und System-Tray-Icon unter Windows sowie `gio mount` / `.desktop`-MIME unter Linux.
|
||||||
|
- **Native Synchronisation (`sanctum sync`)**: RSync-artiger bidirektionaler Dateiabgleich (`Push` & `Pull`) mit 1-MB-Chunk-Streaming direkt in SQLite. Umgeht das fundamentale 4-GB-Dateilimit des Windows WebClient-Dienstes (`0x800700DF`) für Dateien beliebiger Größe (5 GB, 50 GB, 500 GB+). Inklusive `--dry-run` Simulation, `--delete` Spiegelung und `--checksum` Hashprüfung.
|
||||||
|
- **In-Place Self-Upgrade (`sanctum upgrade`)**: Aktualisiert das laufende Binary per Knopfdruck direkt über Gitea Releases. Löst Windows File-Locking via `self-replace` und verifiziert jedes Release kryptografisch gegen `SHA256SUMS.txt`.
|
||||||
- **Transparente LZ4-Kompression**: Automatische Kompression von Chunks mit On-the-Fly-Dekompression und adaptivem Fallback.
|
- **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.
|
- **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.
|
- **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`).
|
- **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`).
|
- **Disaster Recovery**: 24-Wort BIP-39 Mnemonic Seed Phrases, konsistente Online-Backups via SQLite Online Backup API und kryptografische Vollprüfung (`sanctum verify`).
|
||||||
- **Statisches Single-Binary**: `sanctum.exe` (~5.3 MB) ohne externe DLL-Abhängigkeiten.
|
- **Statisches Single-Binary**: Standalone-Executables ohne externe DLL-Abhängigkeiten (`sanctum.exe` für Windows, statisches musl-ELF für Linux).
|
||||||
|
|
||||||
|
> 💡 **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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,9 +98,15 @@ sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S
|
|||||||
|
|
||||||
# Mit Inaktivitäts-Timeout (in Sekunden):
|
# Mit Inaktivitäts-Timeout (in Sekunden):
|
||||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --idle-timeout 300
|
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).
|
- **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 automatisch im Explorer geöffnet.
|
- **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.
|
- **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.
|
- **Beenden**: `Ctrl+C` im Terminal oder Rechtsklick im Tray -> "Aushängen & Beenden" führt einen sauberen Unmount, Speicher-Compaction und WAL-Checkpoint durch.
|
||||||
|
|
||||||
@@ -161,6 +171,59 @@ sanctum.exe unregister
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 8. Native Synchronisation (`sanctum sync`)
|
||||||
|
|
||||||
|
Sanctum bietet eine native, rsync-ähnliche Synchronisations-Engine, die Dateien direkt auf Datenbank- und Chunk-Ebene überträgt. Dies löst das fundamentale Problem des Windows WebClient-Dienstes (`mrxdav.sys`), welcher beim Kopieren über gemountete WebDAV-Netzlaufwerke konstruktionsbedingt auf maximal 4 GB pro Datei beschränkt ist (`FileSizeLimitInBytes` / Fehler `0x800700DF`).
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. Vorab-Simulation ausführen (Dry-Run / risikolos prüfen):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Downloads" /Downloads --dry-run
|
||||||
|
|
||||||
|
# 2. Ordner synchronisieren (unfertige Downloads und Temporärdateien ausschließen):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Downloads" /Downloads --exclude "*.crdownload" --exclude "*.tmp"
|
||||||
|
|
||||||
|
# 3. Exakte 1:1 Spiegelung (löscht verwaiste Dateien im Ziel):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Dokumente" /Dokumente --delete
|
||||||
|
|
||||||
|
# 4. Mit kryptografischer Inhaltsprüfung (SHA-256 Hashes vergleichen):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" "C:\Users\User\Projekte" /Projekte --checksum
|
||||||
|
|
||||||
|
# 5. Daten aus dem Tresor wiederherstellen (Pull / Container -> Host):
|
||||||
|
sanctum.exe sync --path "C:\Pfad\tresor.sanctum" /Downloads "D:\Wiederhergestellt\Downloads" --pull
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Dateigrößen**: Dateien beliebiger Größe (**5 GB, 50 GB, 500 GB+**) werden in 1-MB-Streaming-Chunks mit AES-256-GCM direkt übertragen.
|
||||||
|
- **Fast-Delta Check**: Dateien mit identischer Größe und Modifikationszeitstempel (`mtime`) werden sofort übersprungen.
|
||||||
|
- **Metadaten-Erhaltung**: Datei-Zeitstempel (`mtime`) werden in beide Richtungen exakt konserviert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. In-Place Self-Upgrade (`sanctum upgrade`)
|
||||||
|
|
||||||
|
Aktualisiert die laufende Sanctum-Executable direkt über die offizielle Gitea-Releases-API auf die neueste Version, ohne manuelle Browser-Downloads oder separate Paketmanager:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Nur prüfen, ob ein Update verfügbar ist:
|
||||||
|
sanctum.exe upgrade --check
|
||||||
|
# oder mit dem Alias:
|
||||||
|
sanctum.exe update -c
|
||||||
|
|
||||||
|
# Interaktives Upgrade durchführen (zeigt Release-Details und fragt nach Bestätigung):
|
||||||
|
sanctum.exe upgrade
|
||||||
|
|
||||||
|
# Vollautomatisches Upgrade ohne Rückfragen (z. B. in Skripten):
|
||||||
|
sanctum.exe upgrade -y
|
||||||
|
|
||||||
|
# Upgrade erzwingen (Neuinstallation / Reinstall derselben Version):
|
||||||
|
sanctum.exe upgrade --force
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Plattformunabhängig**: Löst das Windows-Dateilock (`ERROR_ACCESS_DENIED`) via `self-replace` und unterstützt ebenso Linux (statisches musl ELF).
|
||||||
|
- **Kryptografische Integrität**: Das heruntergeladene Binary wird vor dem Ersetzen zwingend gegen die offizielle `SHA256SUMS.txt` via SHA-256 verifiziert.
|
||||||
|
- **Paketmanager-Harmonie**: Erkennt automatisch Installationen via Scoop (`scoop update sanctum`) oder Winget (`winget upgrade HaraldPansi.Sanctum`) und warnt den Benutzer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
## 🛡️ OpSec & Explorer Anti-Leak Shield
|
||||||
|
|
||||||
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
||||||
@@ -169,7 +232,9 @@ Sanctum schützt vertrauliche Daten vor unbeabsichtigten Windows-Spuren:
|
|||||||
2. **Inactivity Auto-Lock**: Erkennt Inaktivität anhand echter Lese-/Schreibzugriffe und trennt den Container automatisch nach Erreichen des Timeouts.
|
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.
|
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.
|
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. **DFIR ShellBag-Schutz**: Unterdrückt das automatische Öffnen des Windows Explorers für den Hidden Vault, um zu verhindern, dass vertrauliche Ordnernamen persistent in `UsrClass.dat` protokolliert werden.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -201,13 +266,30 @@ powershell -ExecutionPolicy Bypass -File .\scripts\package-release.ps1
|
|||||||
```
|
```
|
||||||
|
|
||||||
Erzeugt:
|
Erzeugt:
|
||||||
- `dist/sanctum-v0.3.0-windows-x86_64.zip`
|
- `dist/sanctum-v0.6.0-windows-x86_64.zip` (Windows x86_64 ZIP)
|
||||||
|
- `dist/sanctum-v0.6.0-linux-x86_64.tar.gz` (Linux x86_64 musl static TAR.GZ via `package-release-linux.ps1`)
|
||||||
- `dist/SHA256SUMS.txt`
|
- `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
|
## 📄 Lizenz & Changelog
|
||||||
|
|
||||||
- Lizenziert unter der [MIT License](LICENSE).
|
- 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).
|
- 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.
|
||||||
|
```
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": "0.6.0",
|
||||||
|
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
||||||
|
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
||||||
|
"license": "MIT",
|
||||||
|
"architecture": {
|
||||||
|
"64bit": {
|
||||||
|
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.6.0/sanctum-v0.6.0-windows-x86_64.zip",
|
||||||
|
"hash": "5f2a66bd5ff2ab322b92108fde243c2c5c8d331d55ea98b76f0c94cd8c536a69",
|
||||||
|
"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.6.0
|
||||||
|
PackageName: Sanctum
|
||||||
|
Publisher: Harald Pansi
|
||||||
|
PublisherUrl: https://gitea.pansi.eu/harald
|
||||||
|
PublisherSupportUrl: https://gitea.pansi.eu/harald/sanctum/issues
|
||||||
|
Author: Harald Pansi
|
||||||
|
License: MIT
|
||||||
|
LicenseUrl: https://gitea.pansi.eu/harald/sanctum/src/branch/main/LICENSE
|
||||||
|
ShortDescription: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV
|
||||||
|
Description: Sanctum ist eine eigenständige, speichersichere CLI-Anwendung in Rust, die verschlüsselte Ein-Datei-Container (.sanctum) unter Windows 10/11 im reinen Userland verwaltet. 100% Userland, keine Adminrechte, keine Treiber.
|
||||||
|
Moniker: sanctum
|
||||||
|
Tags:
|
||||||
|
- encryption
|
||||||
|
- security
|
||||||
|
- privacy
|
||||||
|
- webdav
|
||||||
|
- container
|
||||||
|
- plausible-deniability
|
||||||
|
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.6.0
|
||||||
|
Installers:
|
||||||
|
- Architecture: x64
|
||||||
|
InstallerType: zip
|
||||||
|
NestedInstallerType: portable
|
||||||
|
NestedInstallerFiles:
|
||||||
|
- RelativeFilePath: sanctum.exe
|
||||||
|
PortableCommandAlias: sanctum
|
||||||
|
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.6.0/sanctum-v0.6.0-windows-x86_64.zip
|
||||||
|
InstallerSha256: 5f2a66bd5ff2ab322b92108fde243c2c5c8d331d55ea98b76f0c94cd8c536a69
|
||||||
|
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,15 +55,22 @@ $PackageName = "sanctum-v$Version-windows-x86_64"
|
|||||||
$StagingDir = Join-Path $DistDir $PackageName
|
$StagingDir = Join-Path $DistDir $PackageName
|
||||||
$ZipFile = Join-Path $DistDir "$PackageName.zip"
|
$ZipFile = Join-Path $DistDir "$PackageName.zip"
|
||||||
|
|
||||||
if (Test-Path $DistDir) {
|
if (-not (Test-Path $DistDir)) {
|
||||||
Remove-Item $DistDir -Recurse -Force
|
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path $StagingDir) {
|
||||||
|
Remove-Item $StagingDir -Recurse -Force
|
||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
|
||||||
|
|
||||||
$ExeSource = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
$ExeSource = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
||||||
Copy-Item $ExeSource (Join-Path $StagingDir "sanctum.exe")
|
Copy-Item $ExeSource (Join-Path $StagingDir "sanctum.exe")
|
||||||
Copy-Item (Join-Path $ProjectRoot "README.md") (Join-Path $StagingDir "README.md")
|
Copy-Item (Join-Path $ProjectRoot "README.md") (Join-Path $StagingDir "README.md")
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "QUICKSTART.md") (Join-Path $StagingDir "QUICKSTART.md")
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "INSTALL.md") (Join-Path $StagingDir "INSTALL.md")
|
||||||
Copy-Item (Join-Path $ProjectRoot "LICENSE") (Join-Path $StagingDir "LICENSE")
|
Copy-Item (Join-Path $ProjectRoot "LICENSE") (Join-Path $StagingDir "LICENSE")
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "LEGAL.md") (Join-Path $StagingDir "LEGAL.md")
|
||||||
|
Copy-Item (Join-Path $ProjectRoot "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 "CHANGELOG.md") (Join-Path $StagingDir "CHANGELOG.md")
|
||||||
Copy-Item (Join-Path $ProjectRoot "assets") (Join-Path $StagingDir "assets") -Recurse
|
Copy-Item (Join-Path $ProjectRoot "assets") (Join-Path $StagingDir "assets") -Recurse
|
||||||
|
|
||||||
@@ -80,7 +87,17 @@ $ZipHash = (Get-FileHash -Path $ZipFile -Algorithm SHA256).Hash.ToLower()
|
|||||||
$ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower()
|
$ExeHash = (Get-FileHash -Path $ExeSource -Algorithm SHA256).Hash.ToLower()
|
||||||
|
|
||||||
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
|
$ChecksumFile = Join-Path $DistDir "SHA256SUMS.txt"
|
||||||
@("$ZipHash $PackageName.zip", "$ExeHash sanctum.exe") | Set-Content -Path $ChecksumFile -Encoding utf8
|
$ExistingLines = if (Test-Path $ChecksumFile) {
|
||||||
|
Get-Content $ChecksumFile | Where-Object { $_ -notmatch "$PackageName\.zip" -and $_ -notmatch "\s+sanctum\.exe$" }
|
||||||
|
} else {
|
||||||
|
@()
|
||||||
|
}
|
||||||
|
|
||||||
|
$AllLines = @()
|
||||||
|
$AllLines += "$ZipHash $PackageName.zip"
|
||||||
|
$AllLines += "$ExeHash sanctum.exe"
|
||||||
|
$AllLines += $ExistingLines
|
||||||
|
$AllLines | Set-Content -Path $ChecksumFile -Encoding utf8
|
||||||
|
|
||||||
Write-Host "[OK] Pruefsummen in SHA256SUMS.txt gespeichert." -ForegroundColor Green
|
Write-Host "[OK] Pruefsummen in SHA256SUMS.txt gespeichert." -ForegroundColor Green
|
||||||
|
|
||||||
|
|||||||
+47
-21
@@ -108,29 +108,55 @@ if (-not $Release) {
|
|||||||
$UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets"
|
$UploadUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets"
|
||||||
$AuthHeader = "Authorization: token $Token"
|
$AuthHeader = "Authorization: token $Token"
|
||||||
|
|
||||||
# ZIP Asset hochladen
|
# Hilfsfunktion zum sauberen Hochladen/Ersetzen von Assets
|
||||||
Write-Host "Lade sanctum-$TagName-windows-x86_64.zip hoch..." -ForegroundColor Cyan
|
function Upload-ReleaseAsset {
|
||||||
$ZipPathNorm = $ZipFile.Replace('\', '/')
|
param (
|
||||||
$ZipUrl = "$UploadUrl?name=sanctum-$TagName-windows-x86_64.zip"
|
[string]$FilePath,
|
||||||
$ZipResult = & curl.exe -s -X POST $ZipUrl -H $AuthHeader -F "attachment=@$ZipPathNorm"
|
[string]$AssetName
|
||||||
Write-Host "[OK] sanctum-$TagName-windows-x86_64.zip hochgeladen." -ForegroundColor Green
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $FilePath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Lade $AssetName hoch..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Vorhandenes Asset mit gleichem Namen ermitteln und ggf. loeschen
|
||||||
|
$ExistingAssets = try {
|
||||||
|
Invoke-RestMethod -Uri $UploadUrl -Headers $Headers -Method Get
|
||||||
|
} catch {
|
||||||
|
@()
|
||||||
|
}
|
||||||
|
$Duplicate = $ExistingAssets | Where-Object { $_.name -eq $AssetName }
|
||||||
|
if ($Duplicate) {
|
||||||
|
Write-Host " [i] Ersetze existierendes Asset '$AssetName' (ID: $($Duplicate.id))..." -ForegroundColor DarkYellow
|
||||||
|
$DeleteUrl = "$GiteaUrl/api/v1/repos/$Owner/$Repo/releases/$($Release.id)/assets/$($Duplicate.id)"
|
||||||
|
Invoke-RestMethod -Uri $DeleteUrl -Headers $Headers -Method Delete | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$PathNorm = $FilePath.Replace('\', '/')
|
||||||
|
$TargetUrl = "${UploadUrl}?name=$AssetName"
|
||||||
|
$Result = & curl.exe -sS -X POST $TargetUrl -H $AuthHeader -F "attachment=@$PathNorm"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Upload von $AssetName fehlgeschlagen: $Result"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "[OK] $AssetName erfolgreich hochgeladen." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Windows-Assets hochladen
|
||||||
|
Upload-ReleaseAsset -FilePath $ZipFile -AssetName "sanctum-$TagName-windows-x86_64.zip"
|
||||||
|
$ExeFile = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
||||||
|
Upload-ReleaseAsset -FilePath $ExeFile -AssetName "sanctum.exe"
|
||||||
|
|
||||||
|
# Linux-Assets hochladen
|
||||||
|
$LinuxTar = Join-Path $DistDir "sanctum-$TagName-linux-x86_64.tar.gz"
|
||||||
|
Upload-ReleaseAsset -FilePath $LinuxTar -AssetName "sanctum-$TagName-linux-x86_64.tar.gz"
|
||||||
|
$LinuxElf = Join-Path $ProjectRoot "target\x86_64-unknown-linux-musl\release\sanctum"
|
||||||
|
Upload-ReleaseAsset -FilePath $LinuxElf -AssetName "sanctum"
|
||||||
|
|
||||||
# SHA256SUMS.txt hochladen
|
# SHA256SUMS.txt hochladen
|
||||||
Write-Host "Lade SHA256SUMS.txt hoch..." -ForegroundColor Cyan
|
Upload-ReleaseAsset -FilePath $ChecksumFile -AssetName "SHA256SUMS.txt"
|
||||||
$SumPathNorm = $ChecksumFile.Replace('\', '/')
|
|
||||||
$SumUrl = "$UploadUrl?name=SHA256SUMS.txt"
|
|
||||||
$SumResult = & curl.exe -s -X POST $SumUrl -H $AuthHeader -F "attachment=@$SumPathNorm"
|
|
||||||
Write-Host "[OK] SHA256SUMS.txt hochgeladen." -ForegroundColor Green
|
|
||||||
|
|
||||||
# Standalone EXE hochladen
|
|
||||||
$ExeFile = Join-Path $ProjectRoot "target\release\sanctum.exe"
|
|
||||||
if (Test-Path $ExeFile) {
|
|
||||||
Write-Host "Lade sanctum.exe hoch..." -ForegroundColor Cyan
|
|
||||||
$ExePathNorm = $ExeFile.Replace('\', '/')
|
|
||||||
$ExeUrl = "$UploadUrl?name=sanctum.exe"
|
|
||||||
$ExeResult = & curl.exe -s -X POST $ExeUrl -H $AuthHeader -F "attachment=@$ExePathNorm"
|
|
||||||
Write-Host "[OK] sanctum.exe hochgeladen." -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Write-Host "`n============================================================" -ForegroundColor Green
|
Write-Host "`n============================================================" -ForegroundColor Green
|
||||||
|
|||||||
+139
-9
@@ -320,16 +320,121 @@ pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
|
|||||||
Ok(mnemonic.to_string())
|
Ok(mnemonic.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dekodiert eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase zurück in den 32-Byte DEK.
|
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
|
||||||
/// Validiert dabei Wörter und die integrierte BIP-39 Prüfsumme.
|
let a_chars: Vec<char> = a.chars().collect();
|
||||||
pub fn mnemonic_to_dek(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
|
let b_chars: Vec<char> = b.chars().collect();
|
||||||
let cleaned = phrase
|
let (m, n) = (a_chars.len(), b_chars.len());
|
||||||
.split_whitespace()
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
let mnemonic = bip39::Mnemonic::parse_normalized(&cleaned)
|
let mut dp = vec![vec![0usize; n + 1]; m + 1];
|
||||||
.map_err(|e| anyhow::anyhow!("Ungültige BIP-39 Notfallphrase (Wortfehler oder ungültige Prüfsumme): {e}"))?;
|
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();
|
let entropy = mnemonic.to_entropy();
|
||||||
if entropy.len() != 32 {
|
if entropy.len() != 32 {
|
||||||
@@ -625,6 +730,31 @@ mod tests {
|
|||||||
assert!(mnemonic_to_dek(&corrupted_phrase).is_err(), "Checksum check must fail");
|
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]
|
#[test]
|
||||||
fn test_hidden_node_name_encryption_and_dummy_slot() {
|
fn test_hidden_node_name_encryption_and_dummy_slot() {
|
||||||
let dek = generate_dek();
|
let dek = generate_dek();
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
pub mod carrier;
|
pub mod carrier;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod mount;
|
pub mod mount;
|
||||||
|
pub mod platform;
|
||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
pub mod sync;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub mod upgrade;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
pub mod vfs;
|
pub mod vfs;
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|||||||
+347
-28
@@ -68,16 +68,22 @@ enum Commands {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt standardmäßig automatisch das nächste freie Laufwerk)
|
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt unter Windows standardmäßig das nächste freie Laufwerk)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
drive: Option<String>,
|
drive: Option<String>,
|
||||||
|
|
||||||
|
/// Optionaler lokaler Mount-Pfad unter Linux/macOS (z. B. '/mnt/sanctum' oder '~/vault')
|
||||||
|
#[arg(short = 'm', long = "mount-point", value_name = "DIR")]
|
||||||
|
mount_point: Option<PathBuf>,
|
||||||
|
|
||||||
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
|
|
||||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage)
|
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage).
|
||||||
#[arg(long)]
|
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel sicher und interaktiv
|
||||||
|
/// einzugeben (verhindert Klartext-Einträge in der PowerShell-Historie).
|
||||||
|
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||||
recovery_key: Option<String>,
|
recovery_key: Option<String>,
|
||||||
|
|
||||||
/// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer
|
/// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer
|
||||||
@@ -99,6 +105,10 @@ enum Commands {
|
|||||||
/// Deaktiviert das Blockieren und Verbergen von Windows Explorer Metadaten (Thumbs.db, desktop.ini)
|
/// Deaktiviert das Blockieren und Verbergen von Windows Explorer Metadaten (Thumbs.db, desktop.ini)
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_anti_leak: bool,
|
no_anti_leak: bool,
|
||||||
|
|
||||||
|
/// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben)
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
stealth: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||||
@@ -114,8 +124,9 @@ enum Commands {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (erlaubt Reset bei vergessenem Passwort)
|
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (erlaubt Reset bei vergessenem Passwort).
|
||||||
#[arg(long)]
|
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel interaktiv einzugeben.
|
||||||
|
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||||
recovery_key: Option<String>,
|
recovery_key: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -162,8 +173,9 @@ enum Commands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
header_file: Option<PathBuf>,
|
header_file: Option<PathBuf>,
|
||||||
|
|
||||||
/// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort
|
/// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort.
|
||||||
#[arg(long)]
|
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel interaktiv einzugeben.
|
||||||
|
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||||
recovery_key: Option<String>,
|
recovery_key: Option<String>,
|
||||||
|
|
||||||
/// Ziel-Slot für die Wiederherstellung (0 = Decoy/Standard, 1 = Hidden Vault; Standard: 0)
|
/// Ziel-Slot für die Wiederherstellung (0 = Decoy/Standard, 1 = Hidden Vault; Standard: 0)
|
||||||
@@ -189,11 +201,73 @@ enum Commands {
|
|||||||
full: bool,
|
full: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Synchronisiert Dateien und Ordner zwischen Host und Container (rsync-artig, ohne Größenbeschränkung)
|
||||||
|
Sync {
|
||||||
|
/// Pfad zur .sanctum Containerdatei
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Quellpfad (lokales Verzeichnis/Datei oder Vault-Pfad bei --pull)
|
||||||
|
source: String,
|
||||||
|
|
||||||
|
/// Zielpfad (Vault-Pfad wie z. B. '/Downloads' oder lokaler Pfad bei --pull; Standard: '/')
|
||||||
|
#[arg(default_value = "/")]
|
||||||
|
target: String,
|
||||||
|
|
||||||
|
/// Kehrt die Richtung um: Lädt Daten aus dem Tresor auf die lokale Festplatte (Pull)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pull: bool,
|
||||||
|
|
||||||
|
/// Löscht Dateien im Ziel, die an der Quelle nicht mehr existieren (Spiegelung)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
delete: bool,
|
||||||
|
|
||||||
|
/// Führt eine Simulation aus: Zeigt Änderungen an, ohne Dateien zu schreiben oder zu löschen
|
||||||
|
#[arg(short = 'n', long, default_value_t = false)]
|
||||||
|
dry_run: bool,
|
||||||
|
|
||||||
|
/// Vergleicht Dateien anhand kryptografischer Hashes statt nur Zeitstempel und Dateigröße
|
||||||
|
#[arg(short = 'c', long, default_value_t = false)]
|
||||||
|
checksum: bool,
|
||||||
|
|
||||||
|
/// Schließt Dateien oder Ordner anhand von Mustern aus (z. B. '*.tmp', '*.crdownload')
|
||||||
|
#[arg(long)]
|
||||||
|
exclude: Vec<String>,
|
||||||
|
|
||||||
|
/// Unterdrückt Pro-Datei-Ausgaben
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
quiet: bool,
|
||||||
|
|
||||||
|
/// Optionaler 24-Wort Notfallschlüssel (umgeht Passwortabfrage)
|
||||||
|
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||||
|
recovery_key: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
||||||
Register,
|
Register,
|
||||||
|
|
||||||
/// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry
|
/// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry
|
||||||
Unregister,
|
Unregister,
|
||||||
|
|
||||||
|
/// Aktualisiert Sanctum auf die neueste Version (In-Place Self-Upgrade)
|
||||||
|
#[command(alias = "update")]
|
||||||
|
Upgrade {
|
||||||
|
/// Prüft nur auf verfügbare Aktualisierungen, ohne Dateien herunterzuladen
|
||||||
|
#[arg(short = 'c', long, default_value_t = false)]
|
||||||
|
check: bool,
|
||||||
|
|
||||||
|
/// Führt die Aktualisierung ohne interaktive Bestätigung durch
|
||||||
|
#[arg(short = 'y', long, default_value_t = false)]
|
||||||
|
yes: bool,
|
||||||
|
|
||||||
|
/// Erzwingt die Aktualisierung (auch bei gleicher Version oder erkannter Paketmanager-Verwaltung)
|
||||||
|
#[arg(short = 'f', long, default_value_t = false)]
|
||||||
|
force: bool,
|
||||||
|
|
||||||
|
/// Benutzerdefinierte Gitea-Basis-URL (Standard: https://gitea.pansi.eu)
|
||||||
|
#[arg(long, default_value = "https://gitea.pansi.eu")]
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_drive_letter(s: &str) -> Result<char> {
|
fn parse_drive_letter(s: &str) -> Result<char> {
|
||||||
@@ -228,6 +302,19 @@ pub fn parse_size_string(s: &str) -> Result<u64> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Liest den 24-Wort BIP-39 Notfallschlüssel interaktiv und maskiert ein,
|
||||||
|
/// ohne dass Eingaben in der PowerShell-Historie (ConsoleHost_history.txt) landen.
|
||||||
|
fn prompt_recovery_key_interactive() -> Result<Zeroizing<String>> {
|
||||||
|
println!(" [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.", ui::cyan("ℹ"));
|
||||||
|
println!(" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der PowerShell-Historie.", ui::dim("OpSec"));
|
||||||
|
let phrase = rpassword::prompt_password("24-Wort Notfallschlüssel: ")
|
||||||
|
.context("Fehler beim Einlesen des Notfallschlüssels")?;
|
||||||
|
if phrase.trim().is_empty() {
|
||||||
|
bail!("Der Notfallschlüssel darf nicht leer sein.");
|
||||||
|
}
|
||||||
|
Ok(Zeroizing::new(phrase))
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_init(
|
fn handle_init(
|
||||||
container_path: &Path,
|
container_path: &Path,
|
||||||
with_hidden: bool,
|
with_hidden: bool,
|
||||||
@@ -506,8 +593,13 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
let (dek, target_slot_id, carrier_dek, carrier_node_id) = if let Some(phrase) = recovery_key {
|
let (dek, target_slot_id, carrier_dek, carrier_node_id) = if let Some(phrase) = recovery_key {
|
||||||
|
let actual_phrase = if phrase.trim().is_empty() {
|
||||||
|
prompt_recovery_key_interactive()?
|
||||||
|
} else {
|
||||||
|
Zeroizing::new(phrase.to_string())
|
||||||
|
};
|
||||||
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
||||||
let d = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
let d = mnemonic_to_dek(&actual_phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||||||
(d, 0, None, None)
|
(d, 0, None, None)
|
||||||
} else {
|
} else {
|
||||||
@@ -717,7 +809,37 @@ fn handle_restore_header(
|
|||||||
ui::step(2, 2, "💾", "Header in Container-Datenbank zurückgeschrieben!");
|
ui::step(2, 2, "💾", "Header in Container-Datenbank zurückgeschrieben!");
|
||||||
println!();
|
println!();
|
||||||
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
|
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
|
||||||
} else if let Some(key) = recovery_key {
|
} else {
|
||||||
|
let key_str = if let Some(key) = recovery_key {
|
||||||
|
if key.trim().is_empty() {
|
||||||
|
prompt_recovery_key_interactive()?
|
||||||
|
} else {
|
||||||
|
Zeroizing::new(key.to_string())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Interaktiver Auswahldialog statt Fehlermeldung
|
||||||
|
println!(" Keine Wiederherstellungsquelle als CLI-Argument angegeben.");
|
||||||
|
println!(" Bitte wählen Sie die gewünschte Wiederherstellungsmethode:");
|
||||||
|
println!(" [1] Sicherungsdatei verwenden (.sanctum.hdr)");
|
||||||
|
println!(" [2] 24-Wort BIP-39 Notfallschlüssel interaktiv eingeben");
|
||||||
|
print!(" Auswahl [1/2]: ");
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
let mut choice = String::new();
|
||||||
|
std::io::stdin().read_line(&mut choice)?;
|
||||||
|
match choice.trim() {
|
||||||
|
"1" => {
|
||||||
|
print!(" Pfad zur .sanctum.hdr Sicherungsdatei: ");
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
let mut p = String::new();
|
||||||
|
std::io::stdin().read_line(&mut p)?;
|
||||||
|
let hdr_path = PathBuf::from(p.trim());
|
||||||
|
return handle_restore_header(container_path, Some(&hdr_path), None, slot);
|
||||||
|
}
|
||||||
|
"2" => prompt_recovery_key_interactive()?,
|
||||||
|
_ => bail!("Ungültige Auswahl. Wiederherstellung abgebrochen."),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", slot);
|
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", slot);
|
||||||
let new_password = Zeroizing::new(
|
let new_password = Zeroizing::new(
|
||||||
rpassword::prompt_password("Neues Master-Passwort festlegen: ")
|
rpassword::prompt_password("Neues Master-Passwort festlegen: ")
|
||||||
@@ -738,12 +860,10 @@ fn handle_restore_header(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui::step(1, 2, "🔑", "Dekodiere DEK & leite neuen KEK ab...");
|
ui::step(1, 2, "🔑", "Dekodiere DEK & leite neuen KEK ab...");
|
||||||
restore_slot_from_recovery_key(container_path, key, &new_password, slot)?;
|
restore_slot_from_recovery_key(container_path, &key_str, &new_password, slot)?;
|
||||||
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", slot));
|
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", slot));
|
||||||
println!();
|
println!();
|
||||||
println!("{}", ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!"));
|
println!("{}", ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!"));
|
||||||
} else {
|
|
||||||
bail!("Bitte geben Sie entweder --header-file <PFAD> oder --recovery-key \"<24 WÖRTER>\" an.");
|
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@@ -774,12 +894,8 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
|||||||
.authenticate(&password)
|
.authenticate(&password)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||||
let dek = keys.dek().clone();
|
let dek = keys.dek().clone();
|
||||||
let slot_id = keys.slot_id();
|
|
||||||
|
|
||||||
let phrase = dek_to_mnemonic(&dek)?;
|
let phrase = dek_to_mnemonic(&dek)?;
|
||||||
if slot_id == 1 {
|
|
||||||
println!(" • Vault: {}", ui::magenta("Hidden Vault (Slot 1)"));
|
|
||||||
}
|
|
||||||
ui::print_recovery_phrase_card(&phrase);
|
ui::print_recovery_phrase_card(&phrase);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -807,9 +923,7 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
|||||||
|
|
||||||
match meta.authenticate(&password) {
|
match meta.authenticate(&password) {
|
||||||
Some(keys) => {
|
Some(keys) => {
|
||||||
let slot_id = keys.slot_id();
|
println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"));
|
||||||
let vault_desc = if slot_id == 1 { "Hidden Vault (Slot 1)" } else { "Decoy Vault (Slot 0)" };
|
|
||||||
println!(" {} Master-Passwort verifiziert ({}). Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"), ui::cyan(vault_desc));
|
|
||||||
Some(keys.dek().clone())
|
Some(keys.dek().clone())
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -832,6 +946,159 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_sync(
|
||||||
|
container_path: &Path,
|
||||||
|
source: &str,
|
||||||
|
target: &str,
|
||||||
|
pull: bool,
|
||||||
|
delete: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
checksum: bool,
|
||||||
|
exclude: Vec<String>,
|
||||||
|
quiet: bool,
|
||||||
|
recovery_key: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !container_path.exists() {
|
||||||
|
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth = if let Some(key_arg) = recovery_key {
|
||||||
|
let phrase_str = if key_arg.trim().is_empty() {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ 🔑 Maskierte interaktive Notfallschlüssel-Eingabe │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!();
|
||||||
|
rpassword::prompt_password("24-Wort Notfall-Wiederherstellungsschlüssel: ")
|
||||||
|
.context("Fehler beim Einlesen des Schlüssels")?
|
||||||
|
} else {
|
||||||
|
key_arg.to_string()
|
||||||
|
};
|
||||||
|
ContainerAuth::RecoveryKey(Zeroizing::new(phrase_str))
|
||||||
|
} else {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ Sanctum — Ordner- & Dateisynchronisation (Sync) │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" Container: {}", container_path.display());
|
||||||
|
println!();
|
||||||
|
let pass = rpassword::prompt_password("Master-Passwort: ")
|
||||||
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
|
ContainerAuth::Password(Zeroizing::new(pass))
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = Database::open(container_path)
|
||||||
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
|
let (dek, version, vault_id) = match auth {
|
||||||
|
ContainerAuth::Password(ref password) => {
|
||||||
|
let keys = meta
|
||||||
|
.authenticate(password)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||||
|
(keys.dek().clone(), meta.version, keys.slot_id())
|
||||||
|
}
|
||||||
|
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||||
|
let dek = mnemonic_to_dek(phrase)
|
||||||
|
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
|
let is_hidden = {
|
||||||
|
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||||
|
!children.is_empty()
|
||||||
|
};
|
||||||
|
let v_id = if is_hidden { 1 } else { 0 };
|
||||||
|
(dek, meta.version, v_id)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Richtung ermitteln
|
||||||
|
let direction = if pull || source.starts_with('/') {
|
||||||
|
sanctum::sync::SyncDirection::Pull
|
||||||
|
} else {
|
||||||
|
sanctum::sync::SyncDirection::Push
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = sanctum::sync::SyncOptions {
|
||||||
|
direction,
|
||||||
|
delete,
|
||||||
|
dry_run,
|
||||||
|
checksum,
|
||||||
|
exclude_patterns: exclude,
|
||||||
|
quiet,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
let dir_str = match direction {
|
||||||
|
sanctum::sync::SyncDirection::Push => "Push (Host -> Container)",
|
||||||
|
sanctum::sync::SyncDirection::Pull => "Pull (Container -> Host)",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!();
|
||||||
|
if dry_run {
|
||||||
|
println!(" {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
|
println!(" • Richtung: {}", ui::cyan(dir_str));
|
||||||
|
println!(" • Quelle: {}", source);
|
||||||
|
println!(" • Ziel: {}", target);
|
||||||
|
if delete {
|
||||||
|
println!(" • Spiegelung: {} (Dateien im Ziel ohne Entsprechung werden gelöscht)", ui::red("--delete aktiv"));
|
||||||
|
}
|
||||||
|
if checksum {
|
||||||
|
println!(" • Modus: Kryptografischer Inhaltsabgleich (--checksum aktiv)");
|
||||||
|
}
|
||||||
|
if !options.exclude_patterns.is_empty() {
|
||||||
|
println!(" • Filter: {} Ausschlussmuster", options.exclude_patterns.len());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!(" {} Starte Synchronisation ...", ui::dim("[-]"));
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = sanctum::sync::run_sync(
|
||||||
|
&db,
|
||||||
|
vault_id,
|
||||||
|
&dek,
|
||||||
|
version,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
&options,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !dry_run {
|
||||||
|
let _ = db.checkpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
println!();
|
||||||
|
if dry_run {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ℹ Dry-Run Simulation erfolgreich abgeschlossen │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
||||||
|
println!(" • Zu übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
||||||
|
println!(" • Bereits aktuell: {} Dateien (übersprungen)", stats.files_skipped);
|
||||||
|
if delete {
|
||||||
|
println!(" • Zu löschen: {} Dateien/Ordner", stats.files_deleted);
|
||||||
|
}
|
||||||
|
println!(" • Hinweis: {} Es wurden keine Änderungen vorgenommen.", ui::yellow("✔"));
|
||||||
|
} else {
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ✔ Synchronisation erfolgreich abgeschlossen │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
||||||
|
println!(" • Übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
||||||
|
println!(" • Übersprungen: {} Dateien (bereits aktuell)", stats.files_skipped);
|
||||||
|
if delete {
|
||||||
|
println!(" • Gelöscht: {} verwaiste Dateien/Ordner", stats.files_deleted);
|
||||||
|
}
|
||||||
|
let secs = stats.elapsed.as_secs_f64();
|
||||||
|
let mb = (stats.bytes_transferred as f64) / (1024.0 * 1024.0);
|
||||||
|
let speed = if secs > 0.05 { mb / secs } else { 0.0 };
|
||||||
|
println!(" • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)", secs, speed);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn run() -> Result<()> {
|
async fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
@@ -856,6 +1123,7 @@ async fn run() -> Result<()> {
|
|||||||
Commands::Mount {
|
Commands::Mount {
|
||||||
path,
|
path,
|
||||||
drive,
|
drive,
|
||||||
|
mount_point,
|
||||||
port,
|
port,
|
||||||
recovery_key,
|
recovery_key,
|
||||||
no_open,
|
no_open,
|
||||||
@@ -863,21 +1131,36 @@ async fn run() -> Result<()> {
|
|||||||
idle_timeout,
|
idle_timeout,
|
||||||
no_screen_lock,
|
no_screen_lock,
|
||||||
no_anti_leak,
|
no_anti_leak,
|
||||||
|
stealth,
|
||||||
} => {
|
} => {
|
||||||
let drive_char = match drive {
|
let drive_char = match drive {
|
||||||
Some(ref d) => parse_drive_letter(d)?,
|
Some(ref d) => parse_drive_letter(d)?,
|
||||||
None => {
|
None => {
|
||||||
let auto_drive = sanctum::windows::find_next_available_drive()?;
|
#[cfg(windows)]
|
||||||
println!(
|
{
|
||||||
" {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:",
|
let auto_drive = sanctum::windows::find_next_available_drive()?;
|
||||||
ui::cyan("ℹ"),
|
if !stealth {
|
||||||
|
println!(
|
||||||
|
" {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:",
|
||||||
|
ui::cyan("ℹ"),
|
||||||
|
auto_drive
|
||||||
|
);
|
||||||
|
}
|
||||||
auto_drive
|
auto_drive
|
||||||
);
|
}
|
||||||
auto_drive
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
'Z'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let auth = if let Some(key) = recovery_key {
|
let auth = if let Some(key) = recovery_key {
|
||||||
ContainerAuth::RecoveryKey(Zeroizing::new(key))
|
let actual_key = if key.trim().is_empty() {
|
||||||
|
prompt_recovery_key_interactive()?
|
||||||
|
} else {
|
||||||
|
Zeroizing::new(key)
|
||||||
|
};
|
||||||
|
ContainerAuth::RecoveryKey(actual_key)
|
||||||
} else {
|
} else {
|
||||||
let prompt_text = format!(
|
let prompt_text = format!(
|
||||||
"Master-Passwort für Container '{}' eingeben: ",
|
"Master-Passwort für Container '{}' eingeben: ",
|
||||||
@@ -890,13 +1173,14 @@ async fn run() -> Result<()> {
|
|||||||
ContainerAuth::Password(password)
|
ContainerAuth::Password(password)
|
||||||
};
|
};
|
||||||
|
|
||||||
let open_explorer = !no_open;
|
let open_explorer = !no_open && !stealth;
|
||||||
let enable_tray = !no_tray;
|
let enable_tray = !no_tray && !stealth;
|
||||||
let lock_on_screen_lock = !no_screen_lock;
|
let lock_on_screen_lock = !no_screen_lock;
|
||||||
let anti_leak = !no_anti_leak;
|
let anti_leak = !no_anti_leak;
|
||||||
mount_container(
|
mount_container(
|
||||||
&path,
|
&path,
|
||||||
drive_char,
|
drive_char,
|
||||||
|
mount_point.as_deref(),
|
||||||
port,
|
port,
|
||||||
auth,
|
auth,
|
||||||
open_explorer,
|
open_explorer,
|
||||||
@@ -904,6 +1188,7 @@ async fn run() -> Result<()> {
|
|||||||
idle_timeout,
|
idle_timeout,
|
||||||
lock_on_screen_lock,
|
lock_on_screen_lock,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
stealth,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -936,6 +1221,31 @@ async fn run() -> Result<()> {
|
|||||||
Commands::Verify { path, full } => {
|
Commands::Verify { path, full } => {
|
||||||
handle_verify(&path, full)?;
|
handle_verify(&path, full)?;
|
||||||
}
|
}
|
||||||
|
Commands::Sync {
|
||||||
|
path,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
pull,
|
||||||
|
delete,
|
||||||
|
dry_run,
|
||||||
|
checksum,
|
||||||
|
exclude,
|
||||||
|
quiet,
|
||||||
|
recovery_key,
|
||||||
|
} => {
|
||||||
|
handle_sync(
|
||||||
|
&path,
|
||||||
|
&source,
|
||||||
|
&target,
|
||||||
|
pull,
|
||||||
|
delete,
|
||||||
|
dry_run,
|
||||||
|
checksum,
|
||||||
|
exclude,
|
||||||
|
quiet,
|
||||||
|
recovery_key.as_deref(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
Commands::Register => {
|
Commands::Register => {
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
||||||
@@ -956,6 +1266,15 @@ async fn run() -> Result<()> {
|
|||||||
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
Commands::Upgrade { check, yes, force, url } => {
|
||||||
|
let options = sanctum::upgrade::UpgradeOptions {
|
||||||
|
check_only: check,
|
||||||
|
yes,
|
||||||
|
force,
|
||||||
|
base_url: url,
|
||||||
|
};
|
||||||
|
sanctum::upgrade::run_upgrade(&options)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+360
-175
@@ -32,87 +32,119 @@ pub fn format_drive(drive_letter: char) -> String {
|
|||||||
format!("{}:", drive_letter.to_ascii_uppercase())
|
format!("{}:", drive_letter.to_ascii_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`.
|
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y` bzw. Unix-Mountpoint via `gio mount -u`.
|
||||||
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let drive_str = format_drive(drive_letter);
|
||||||
|
|
||||||
let output = Command::new("net")
|
let output = Command::new("net")
|
||||||
.args(["use", &drive_str, "/delete", "/y"])
|
.args(["use", &drive_str, "/delete", "/y"])
|
||||||
.output()
|
.output()
|
||||||
.context("Fehler beim Ausführen des Befehls 'net use'")?;
|
.context("Fehler beim Ausführen des Befehls 'net use'")?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
bail!(
|
bail!(
|
||||||
"Netzlaufwerk {} konnte nicht getrennt werden:\n{}{}",
|
"Netzlaufwerk {} konnte nicht getrennt werden:\n{}{}",
|
||||||
drive_str,
|
drive_str,
|
||||||
stdout,
|
stdout,
|
||||||
stderr
|
stderr
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /persistent:no` ein.
|
|
||||||
fn run_net_use_mount(drive_str: &str, port: u16, session_token: &str) -> Result<()> {
|
|
||||||
let url = format!("http://127.0.0.1:{}/{}/", port, session_token);
|
|
||||||
|
|
||||||
let mut output = Command::new("net")
|
|
||||||
.args(["use", drive_str, &url, "/persistent:no"])
|
|
||||||
.output()
|
|
||||||
.context("Fehler beim Ausführen des Befehls 'net use'")?;
|
|
||||||
|
|
||||||
// PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85)
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let is_already_in_use = stderr.contains("85")
|
|
||||||
|| stderr.contains("bereits verwendet")
|
|
||||||
|| stderr.contains("already in use")
|
|
||||||
|| stdout.contains("85")
|
|
||||||
|| stdout.contains("bereits verwendet")
|
|
||||||
|| stdout.contains("already in use");
|
|
||||||
|
|
||||||
if is_already_in_use {
|
|
||||||
debug!("Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...", drive_str);
|
|
||||||
let _ = Command::new("net")
|
|
||||||
.args(["use", drive_str, "/delete", "/y"])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
// Zweiter Versuch nach automatischer Bereinigung
|
|
||||||
output = Command::new("net")
|
|
||||||
.args(["use", drive_str, &url, "/persistent:no"])
|
|
||||||
.output()
|
|
||||||
.context("Fehler beim erneuten Ausführen des Befehls 'net use'")?;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !output.status.success() {
|
Ok(())
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
}
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
#[cfg(unix)]
|
||||||
let webclient_hint = if stderr.contains("67") || stderr.contains("Netzwerkname") || stderr.contains("Systemfehler") {
|
{
|
||||||
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'."
|
let _ = drive_letter;
|
||||||
} else {
|
Ok(())
|
||||||
""
|
}
|
||||||
};
|
#[cfg(not(any(windows, unix)))]
|
||||||
bail!(
|
{
|
||||||
"Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}",
|
let _ = drive_letter;
|
||||||
drive_str,
|
Ok(())
|
||||||
stdout,
|
|
||||||
stderr,
|
|
||||||
webclient_hint
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk ein.
|
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /persistent:no` ein
|
||||||
|
/// bzw. unter Unix via `gio mount dav://127.0.0.1:<PORT>/<TOKEN>/`.
|
||||||
|
fn run_mount_command(drive_str: &str, port: u16, session_token: &str) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let url = format!("http://127.0.0.1:{}/{}/", port, session_token);
|
||||||
|
|
||||||
|
let mut output = Command::new("net")
|
||||||
|
.args(["use", drive_str, &url, "/persistent:no"])
|
||||||
|
.output()
|
||||||
|
.context("Fehler beim Ausführen des Befehls 'net use'")?;
|
||||||
|
|
||||||
|
// PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85)
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let is_already_in_use = stderr.contains("85")
|
||||||
|
|| stderr.contains("bereits verwendet")
|
||||||
|
|| stderr.contains("already in use")
|
||||||
|
|| stdout.contains("85")
|
||||||
|
|| stdout.contains("bereits verwendet")
|
||||||
|
|| stdout.contains("already in use");
|
||||||
|
|
||||||
|
if is_already_in_use {
|
||||||
|
debug!("Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...", drive_str);
|
||||||
|
let _ = Command::new("net")
|
||||||
|
.args(["use", drive_str, "/delete", "/y"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
// Zweiter Versuch nach automatischer Bereinigung
|
||||||
|
output = Command::new("net")
|
||||||
|
.args(["use", drive_str, &url, "/persistent:no"])
|
||||||
|
.output()
|
||||||
|
.context("Fehler beim erneuten Ausführen des Befehls 'net use'")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let webclient_hint = if stderr.contains("67") || stderr.contains("Netzwerkname") || stderr.contains("Systemfehler") {
|
||||||
|
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'."
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
bail!(
|
||||||
|
"Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}",
|
||||||
|
drive_str,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
webclient_hint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let dav_url = format!("dav://127.0.0.1:{}/{}/", port, session_token);
|
||||||
|
let _ = Command::new("gio")
|
||||||
|
.args(["mount", &dav_url])
|
||||||
|
.output();
|
||||||
|
let _ = drive_str;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = (drive_str, port, session_token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk (Windows) bzw. VFS (Linux) ein.
|
||||||
pub async fn mount_container(
|
pub async fn mount_container(
|
||||||
container_path: &Path,
|
container_path: &Path,
|
||||||
drive_letter: char,
|
drive_letter: char,
|
||||||
|
mount_point: Option<&Path>,
|
||||||
requested_port: Option<u16>,
|
requested_port: Option<u16>,
|
||||||
auth: ContainerAuth,
|
auth: ContainerAuth,
|
||||||
open_explorer: bool,
|
open_explorer: bool,
|
||||||
@@ -120,8 +152,13 @@ pub async fn mount_container(
|
|||||||
idle_timeout: Option<u64>,
|
idle_timeout: Option<u64>,
|
||||||
lock_on_screen_lock: bool,
|
lock_on_screen_lock: bool,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
|
stealth: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
|
let _ = mount_point;
|
||||||
|
let _ = open_explorer;
|
||||||
|
let _ = enable_tray;
|
||||||
|
let _ = lock_on_screen_lock;
|
||||||
|
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -130,8 +167,10 @@ pub async fn mount_container(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
if !stealth {
|
||||||
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
println!();
|
||||||
|
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
||||||
|
}
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
@@ -141,10 +180,14 @@ pub async fn mount_container(
|
|||||||
|
|
||||||
let (dek, carrier_dek, carrier_node_id, version, vault_id) = match auth {
|
let (dek, carrier_dek, carrier_node_id, version, vault_id) = match auth {
|
||||||
ContainerAuth::Password(ref password) => {
|
ContainerAuth::Password(ref password) => {
|
||||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...");
|
if !stealth {
|
||||||
|
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...");
|
||||||
|
}
|
||||||
match meta.authenticate(password) {
|
match meta.authenticate(password) {
|
||||||
Some(keys) => {
|
Some(keys) => {
|
||||||
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
|
if !stealth {
|
||||||
|
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
|
||||||
|
}
|
||||||
let vault_id = keys.slot_id();
|
let vault_id = keys.slot_id();
|
||||||
let ver = keys.version();
|
let ver = keys.version();
|
||||||
let c_dek = keys.carrier_dek();
|
let c_dek = keys.carrier_dek();
|
||||||
@@ -157,10 +200,14 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||||
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
if !stealth {
|
||||||
|
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
||||||
|
}
|
||||||
let dek = mnemonic_to_dek(phrase)
|
let dek = mnemonic_to_dek(phrase)
|
||||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
if !stealth {
|
||||||
|
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann
|
// Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann
|
||||||
let is_hidden = {
|
let is_hidden = {
|
||||||
@@ -212,15 +259,28 @@ pub async fn mount_container(
|
|||||||
let bound_addr = listener.local_addr()?;
|
let bound_addr = listener.local_addr()?;
|
||||||
let bound_port = bound_addr.port();
|
let bound_port = bound_addr.port();
|
||||||
|
|
||||||
ui::step(
|
if !stealth {
|
||||||
4,
|
#[cfg(windows)]
|
||||||
4,
|
ui::step(
|
||||||
"🌐",
|
4,
|
||||||
&format!(
|
4,
|
||||||
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
|
"🌐",
|
||||||
bound_port, drive_str
|
&format!(
|
||||||
),
|
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
|
||||||
);
|
bound_port, drive_str
|
||||||
|
),
|
||||||
|
);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
ui::step(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
"🌐",
|
||||||
|
&format!(
|
||||||
|
"Starte WebDAV-Server auf Port {} & initialisiere VFS...",
|
||||||
|
bound_port
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
||||||
@@ -232,28 +292,22 @@ pub async fn mount_container(
|
|||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Netzlaufwerk einbinden
|
// Netzlaufwerk bzw. Verzeichnis einbinden
|
||||||
if let Err(e) = run_net_use_mount(&drive_str, bound_port, &session_token) {
|
if let Err(e) = run_mount_command(&drive_str, bound_port, &session_token) {
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional automatisch im Windows Explorer öffnen (mit DFIR ShellBag-Schutz für Hidden Vault)
|
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
||||||
|
#[cfg(windows)]
|
||||||
if open_explorer {
|
if open_explorer {
|
||||||
if vault_id == 1 {
|
let _ = crate::windows::open_in_explorer(drive_letter);
|
||||||
println!(
|
|
||||||
" {} {}",
|
|
||||||
ui::yellow("[!]"),
|
|
||||||
ui::dim("OpSec-Schutz: Explorer-Auto-Open für Hidden Vault unterdrückt (verhindert persistente ShellBag-Spuren in UsrClass.dat).")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let _ = crate::windows::open_in_explorer(drive_letter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// System-Tray Initialisierung
|
// System-Tray Initialisierung (nur Windows)
|
||||||
let (tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
#[cfg(windows)]
|
||||||
|
let (_tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let _tray = if enable_tray {
|
let _tray = if enable_tray {
|
||||||
let icon_source = crate::windows::get_default_system_icon()
|
let icon_source = crate::windows::get_default_system_icon()
|
||||||
@@ -271,7 +325,7 @@ pub async fn mount_container(
|
|||||||
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
||||||
let _ = crate::windows::open_in_explorer(dl);
|
let _ = crate::windows::open_in_explorer(dl);
|
||||||
});
|
});
|
||||||
let s_tx = tray_shutdown_tx.clone();
|
let s_tx = _tray_shutdown_tx.clone();
|
||||||
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
||||||
let _ = s_tx.blocking_send(());
|
let _ = s_tx.blocking_send(());
|
||||||
});
|
});
|
||||||
@@ -310,8 +364,10 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows-Sitzungssperre (Win + L Auto-Lock)
|
// Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
|
||||||
|
#[cfg(windows)]
|
||||||
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
|
#[cfg(windows)]
|
||||||
let _session_monitor = if lock_on_screen_lock {
|
let _session_monitor = if lock_on_screen_lock {
|
||||||
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
match crate::windows::start_session_lock_monitor(session_lock_tx) {
|
||||||
Ok(guard) => Some(guard),
|
Ok(guard) => Some(guard),
|
||||||
@@ -324,99 +380,228 @@ pub async fn mount_container(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
println!();
|
if stealth {
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
#[cfg(windows)]
|
||||||
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
#[cfg(not(windows))]
|
||||||
println!();
|
println!("Sanctum: WebDAV-Server auf Port {} bereit.", bound_port);
|
||||||
println!(" • Container: {}", container_path.display());
|
} else {
|
||||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
println!();
|
||||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
if vault_id == 1 {
|
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
||||||
println!(" • Vault: {} (Verschlüsselte Dateinamen)", ui::magenta("Hidden Vault (Slot 1)"));
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!();
|
||||||
|
println!(" • Container: {}", container_path.display());
|
||||||
|
#[cfg(windows)]
|
||||||
|
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if let Some(mp) = mount_point {
|
||||||
|
println!(" • Mountpoint: {}", ui::cyan(&mp.display().to_string()));
|
||||||
|
} else {
|
||||||
|
println!(" • Modus: WebDAV Userland-VFS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
|
||||||
|
#[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!();
|
||||||
|
#[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!();
|
||||||
}
|
}
|
||||||
if let Some(secs) = idle_timeout {
|
|
||||||
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
|
||||||
}
|
|
||||||
if lock_on_screen_lock {
|
|
||||||
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
|
|
||||||
}
|
|
||||||
if anti_leak {
|
|
||||||
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
|
|
||||||
}
|
|
||||||
if enable_tray {
|
|
||||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT)
|
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT, nur Windows)
|
||||||
|
#[cfg(windows)]
|
||||||
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
|
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
|
#[cfg(windows)]
|
||||||
let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok();
|
let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok();
|
||||||
|
|
||||||
|
// Unix Signale (SIGTERM, SIGHUP)
|
||||||
|
#[cfg(unix)]
|
||||||
|
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
|
||||||
|
#[cfg(unix)]
|
||||||
|
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
|
||||||
|
|
||||||
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
|
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen)
|
||||||
|
#[cfg(windows)]
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = tokio::signal::ctrl_c() => {
|
res = tokio::signal::ctrl_c() => {
|
||||||
let _ = res;
|
let _ = res;
|
||||||
println!();
|
if !stealth {
|
||||||
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
println!();
|
||||||
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = console_close_rx.recv() => {
|
Some(()) = console_close_rx.recv() => {
|
||||||
println!();
|
if !stealth {
|
||||||
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
|
println!();
|
||||||
|
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = tray_shutdown_rx.recv() => {
|
Some(()) = tray_shutdown_rx.recv() => {
|
||||||
println!();
|
if !stealth {
|
||||||
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
println!();
|
||||||
|
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = session_lock_rx.recv() => {
|
Some(()) = session_lock_rx.recv() => {
|
||||||
println!();
|
if !stealth {
|
||||||
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
println!();
|
||||||
|
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = idle_shutdown_rx.recv() => {
|
Some(()) = idle_shutdown_rx.recv() => {
|
||||||
println!();
|
if !stealth {
|
||||||
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
println!();
|
||||||
}
|
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||||
}
|
|
||||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
||||||
|
|
||||||
// Automatisches Unmount
|
|
||||||
if let Err(e) = unmount_drive(drive_letter) {
|
|
||||||
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
|
||||||
} else {
|
|
||||||
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
|
#[cfg(unix)]
|
||||||
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
tokio::select! {
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
res = tokio::signal::ctrl_c() => {
|
||||||
if let Err(e) = db.checkpoint() {
|
let _ = res;
|
||||||
println!("{}", ui::red(&format!("Fehler ({e})")));
|
if !stealth {
|
||||||
} else {
|
println!();
|
||||||
println!("{}", ui::green("OK"));
|
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("[!]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
#[cfg(not(any(windows, unix)))]
|
||||||
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔"));
|
tokio::select! {
|
||||||
println!();
|
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());
|
||||||
|
|
||||||
|
// Automatisches Unmount
|
||||||
|
if let Err(e) = unmount_drive(drive_letter) {
|
||||||
|
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
||||||
|
} 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());
|
||||||
|
if let Err(e) = db.checkpoint() {
|
||||||
|
println!("{}", ui::red(&format!("Fehler ({e})")));
|
||||||
|
} else {
|
||||||
|
println!("{}", ui::green("OK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔"));
|
||||||
|
println!();
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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::*;
|
||||||
@@ -281,6 +281,24 @@ impl Database {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prüft, ob ein Knoten (z. B. der Carrier-Knoten) ein Nachfahre (direkt oder indirekt) eines Verzeichnisses ist.
|
||||||
|
pub fn is_descendant_of(&self, node_id: i64, ancestor_id: i64) -> Result<bool> {
|
||||||
|
if node_id == ancestor_id {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"WITH RECURSIVE sub(id) AS (
|
||||||
|
SELECT id FROM nodes WHERE id = ?1
|
||||||
|
UNION ALL
|
||||||
|
SELECT n.id FROM nodes n JOIN sub ON n.parent_id = sub.id
|
||||||
|
)
|
||||||
|
SELECT 1 FROM sub WHERE id = ?2 LIMIT 1;",
|
||||||
|
)?;
|
||||||
|
let exists: Option<i64> = stmt.query_row(params![ancestor_id, node_id], |r| r.get(0)).optional()?;
|
||||||
|
Ok(exists.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
/// Überschreibt Chunks eines Knotens vor dem Löschen mit kryptografischem Zufallsrauschen (Chunk Shredding).
|
/// Überschreibt Chunks eines Knotens vor dem Löschen mit kryptografischem Zufallsrauschen (Chunk Shredding).
|
||||||
pub fn shred_chunks_for_node(&self, node_id: i64) -> Result<()> {
|
pub fn shred_chunks_for_node(&self, node_id: i64) -> Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
@@ -1182,6 +1200,43 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schreibt einen verschlüsselten Chunk und aktualisiert Dateigröße und Modifikationszeitstempel atomar
|
||||||
|
/// in einer einzigen SQLite-Transaktion (Crash-Konsistenz / Power-Loss Schutz / CHAOS-01).
|
||||||
|
pub fn write_chunk_and_update_size(
|
||||||
|
&self,
|
||||||
|
node_id: i64,
|
||||||
|
chunk_index: u32,
|
||||||
|
nonce: &[u8; 12],
|
||||||
|
tag: &[u8; 16],
|
||||||
|
ciphertext: &[u8],
|
||||||
|
new_size: u64,
|
||||||
|
modified_at: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut conn = self.conn.lock().unwrap();
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
|
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
|
||||||
|
nonce = excluded.nonce,
|
||||||
|
tag = excluded.tag,
|
||||||
|
ciphertext = excluded.ciphertext",
|
||||||
|
params![
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
nonce.as_slice(),
|
||||||
|
tag.as_slice(),
|
||||||
|
ciphertext,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE nodes SET size = ?1, modified_at = ?2 WHERE id = ?3",
|
||||||
|
params![new_size as i64, modified_at as i64, node_id],
|
||||||
|
)?;
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Schneidet überzählige Chunks ab (z. B. beim Truncate oder Überschreiben mit kleinerer Datei)
|
/// Schneidet überzählige Chunks ab (z. B. beim Truncate oder Überschreiben mit kleinerer Datei)
|
||||||
/// und shreddert die abzuschneidenden Chunks vorher mit kryptografischem Zufallsrauschen.
|
/// und shreddert die abzuschneidenden Chunks vorher mit kryptografischem Zufallsrauschen.
|
||||||
pub fn truncate_chunks_after(&self, node_id: i64, max_chunk_index: u32) -> Result<()> {
|
pub fn truncate_chunks_after(&self, node_id: i64, max_chunk_index: u32) -> Result<()> {
|
||||||
@@ -1691,4 +1746,48 @@ mod tests {
|
|||||||
).unwrap();
|
).unwrap();
|
||||||
assert_eq!(has_vault_id_chunks, 0, "vault_id darf nicht in chunks existieren");
|
assert_eq!(has_vault_id_chunks, 0, "vault_id darf nicht in chunks existieren");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_atomic_chunk_write_and_size_update() {
|
||||||
|
let db = Database::open_in_memory().unwrap();
|
||||||
|
let (salt, kdf, wrapped_dek, nonce, tag) = (
|
||||||
|
[1u8; 16],
|
||||||
|
KdfParams::default(),
|
||||||
|
vec![2u8; 40],
|
||||||
|
[3u8; 12],
|
||||||
|
[4u8; 16],
|
||||||
|
);
|
||||||
|
db.init_schema(&salt, &kdf, &wrapped_dek, &nonce, &tag).unwrap();
|
||||||
|
|
||||||
|
let file = db.create_node(1, "crash_test.bin", false).unwrap();
|
||||||
|
assert_eq!(file.size, 0);
|
||||||
|
|
||||||
|
let chunk_nonce = [5u8; 12];
|
||||||
|
let chunk_tag = [6u8; 16];
|
||||||
|
let ciphertext = vec![7u8; 1024];
|
||||||
|
let new_size = 1024u64;
|
||||||
|
let modified_at = 2000000u64;
|
||||||
|
|
||||||
|
db.write_chunk_and_update_size(
|
||||||
|
file.id,
|
||||||
|
0,
|
||||||
|
&chunk_nonce,
|
||||||
|
&chunk_tag,
|
||||||
|
&ciphertext,
|
||||||
|
new_size,
|
||||||
|
modified_at,
|
||||||
|
).expect("Atomic write");
|
||||||
|
|
||||||
|
// Chunk verifizieren
|
||||||
|
let chunk = db.read_chunk(file.id, 0).unwrap().expect("Chunk must exist");
|
||||||
|
assert_eq!(chunk.ciphertext, ciphertext);
|
||||||
|
assert_eq!(chunk.nonce, chunk_nonce);
|
||||||
|
assert_eq!(chunk.tag, chunk_tag);
|
||||||
|
|
||||||
|
// Inode verifizieren
|
||||||
|
let nodes = db.list_children(1).unwrap();
|
||||||
|
let updated_file = nodes.iter().find(|n| n.id == file.id).unwrap();
|
||||||
|
assert_eq!(updated_file.size, new_size);
|
||||||
|
assert_eq!(updated_file.modified_at, modified_at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+850
@@ -0,0 +1,850 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
||||||
|
use crate::storage::{Database, NodeRecord};
|
||||||
|
use crate::ui;
|
||||||
|
use crate::vfs::is_leak_file;
|
||||||
|
|
||||||
|
/// Synchronisationsrichtung
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SyncDirection {
|
||||||
|
/// Host-Dateisystem -> Sanctum Container (Upload / Backup)
|
||||||
|
Push,
|
||||||
|
/// Sanctum Container -> Host-Dateisystem (Download / Restore)
|
||||||
|
Pull,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optionen für den Synchronisationslauf
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SyncOptions {
|
||||||
|
pub direction: SyncDirection,
|
||||||
|
pub delete: bool,
|
||||||
|
pub dry_run: bool,
|
||||||
|
pub checksum: bool,
|
||||||
|
pub exclude_patterns: Vec<String>,
|
||||||
|
pub quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SyncOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
direction: SyncDirection::Push,
|
||||||
|
delete: false,
|
||||||
|
dry_run: false,
|
||||||
|
checksum: false,
|
||||||
|
exclude_patterns: Vec::new(),
|
||||||
|
quiet: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistiken über den Synchronisationslauf
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct SyncStats {
|
||||||
|
pub files_scanned: usize,
|
||||||
|
pub files_transferred: usize,
|
||||||
|
pub files_skipped: usize,
|
||||||
|
pub files_deleted: usize,
|
||||||
|
pub bytes_transferred: u64,
|
||||||
|
pub elapsed: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ergebnis einer einzelnen Dateiübertragung
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FileTransferResult {
|
||||||
|
Transferred { size: u64 },
|
||||||
|
Skipped { size: u64 },
|
||||||
|
DryRunTransferred { size: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft, ob ein Dateiname oder Pfad einem der Ausschlussmuster entspricht.
|
||||||
|
pub fn is_excluded(name: &str, rel_path: &str, patterns: &[String]) -> bool {
|
||||||
|
// 1. Anti-Leak Shield: Typische Explorer- und OS-Metadaten immer ausschließen
|
||||||
|
if is_leak_file(name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let norm_rel = rel_path.replace('\\', "/");
|
||||||
|
let norm_name = name.to_ascii_lowercase();
|
||||||
|
|
||||||
|
for pat in patterns {
|
||||||
|
let p = pat.trim();
|
||||||
|
if p.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard-Muster: *.ext
|
||||||
|
if p.starts_with("*.") {
|
||||||
|
let ext = &p[1..].to_ascii_lowercase();
|
||||||
|
if norm_name.ends_with(ext) || norm_rel.to_ascii_lowercase().ends_with(ext) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wildcard-Muster: prefix*
|
||||||
|
else if p.ends_with('*') && !p[..p.len() - 1].contains('*') {
|
||||||
|
let prefix = p[..p.len() - 1].to_ascii_lowercase();
|
||||||
|
if norm_name.starts_with(&prefix) || norm_rel.to_ascii_lowercase().starts_with(&prefix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Exakter Name oder Teilpfad
|
||||||
|
else {
|
||||||
|
let p_lower = p.to_ascii_lowercase();
|
||||||
|
if norm_name == p_lower {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if norm_rel.trim_start_matches('/').to_ascii_lowercase() == p_lower.trim_start_matches('/') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liest bis zu `buf.len()` Bytes aus einer Datei (verlässliche Chunk-Lesung).
|
||||||
|
fn read_chunk_buffer(file: &mut File, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
let mut total = 0;
|
||||||
|
while total < buf.len() {
|
||||||
|
match file.read(&mut buf[total..]) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => total += n,
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt den Modifikationszeitstempel einer lokalen Datei via std::fs::FileTimes.
|
||||||
|
fn set_local_file_mtime(file: &File, mtime_secs: u64) {
|
||||||
|
let times = std::fs::FileTimes::new()
|
||||||
|
.set_modified(UNIX_EPOCH + Duration::from_secs(mtime_secs));
|
||||||
|
let _ = file.set_times(times);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stellt sicher, dass ein Verzeichnispfad im Tresor existiert und gibt den Ziel-Knoten zurück.
|
||||||
|
pub fn ensure_vault_dir_tree(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
vault_dir_path: &str,
|
||||||
|
) -> Result<NodeRecord> {
|
||||||
|
let normalized = vault_dir_path.trim_matches('/');
|
||||||
|
let root_id = Database::get_root_node_id_for_vault(vault_id);
|
||||||
|
let root_node = db
|
||||||
|
.get_node_by_id_in_vault(root_id, vault_id, dek)?
|
||||||
|
.context("Wurzelknoten im Container nicht gefunden")?;
|
||||||
|
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Ok(root_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let segments: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
let mut current_id = root_id;
|
||||||
|
let mut current_node = root_node;
|
||||||
|
|
||||||
|
for segment in segments {
|
||||||
|
let children = db.list_children_in_vault(current_id, vault_id, dek)?;
|
||||||
|
if let Some(existing) = children.into_iter().find(|c| c.name == segment) {
|
||||||
|
if !existing.is_dir {
|
||||||
|
bail!("Pfad-Konflikt: '{}' existiert im Container bereits als Datei", segment);
|
||||||
|
}
|
||||||
|
current_id = existing.id;
|
||||||
|
current_node = existing;
|
||||||
|
} else {
|
||||||
|
let new_dir = db.create_node_in_vault(vault_id, current_id, segment, true, dek)?;
|
||||||
|
current_id = new_dir.id;
|
||||||
|
current_node = new_dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(current_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronisiert eine einzelne Host-Datei in den Tresor (Push).
|
||||||
|
pub fn sync_single_file_to_vault(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
local_path: &Path,
|
||||||
|
parent_node_id: i64,
|
||||||
|
file_name: &str,
|
||||||
|
checksum: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<FileTransferResult> {
|
||||||
|
let meta = fs::metadata(local_path)
|
||||||
|
.with_context(|| format!("Konnte Metadaten für '{}' nicht lesen", local_path.display()))?;
|
||||||
|
let local_size = meta.len();
|
||||||
|
let local_mtime = meta
|
||||||
|
.modified()
|
||||||
|
.unwrap_or(SystemTime::now())
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let children = db.list_children_in_vault(parent_node_id, vault_id, dek)?;
|
||||||
|
let existing_node = children.into_iter().find(|c| c.name == file_name);
|
||||||
|
|
||||||
|
if let Some(ref node) = existing_node {
|
||||||
|
if node.is_dir {
|
||||||
|
bail!("Pfad-Konflikt: '{}' existiert im Tresor als Ordner", file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast Check: Wenn Größe und mtime identisch sind, überspringen
|
||||||
|
if !checksum && node.size == local_size && node.modified_at == local_mtime {
|
||||||
|
return Ok(FileTransferResult::Skipped { size: local_size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
return Ok(FileTransferResult::DryRunTransferred { size: local_size });
|
||||||
|
}
|
||||||
|
|
||||||
|
let node_id = match existing_node {
|
||||||
|
Some(n) => n.id,
|
||||||
|
None => {
|
||||||
|
let new_node = db.create_node_in_vault(vault_id, parent_node_id, file_name, false, dek)?;
|
||||||
|
new_node.id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1-MB-Chunk Streaming direkt in SQLite
|
||||||
|
let mut file = File::open(local_path)
|
||||||
|
.with_context(|| format!("Konnte '{}' nicht zum Lesen öffnen", local_path.display()))?;
|
||||||
|
let mut buffer = vec![0u8; CHUNK_SIZE];
|
||||||
|
let mut chunk_idx = 0u32;
|
||||||
|
let mut bytes_written = 0u64;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let n = read_chunk_buffer(&mut file, &mut buffer)?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_data = &buffer[..n];
|
||||||
|
let (ciphertext, nonce, tag) = encrypt_chunk(dek, node_id, chunk_idx, chunk_data, version)?;
|
||||||
|
bytes_written += n as u64;
|
||||||
|
|
||||||
|
db.write_chunk_and_update_size(
|
||||||
|
node_id,
|
||||||
|
chunk_idx,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
&ciphertext,
|
||||||
|
bytes_written,
|
||||||
|
local_mtime,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
chunk_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bei Überschreiben einer ehemals größeren Datei überzählige alte Chunks entfernen
|
||||||
|
db.truncate_chunks_after(node_id, chunk_idx.saturating_sub(1))?;
|
||||||
|
db.update_node_size_and_time(node_id, local_size, local_mtime)?;
|
||||||
|
|
||||||
|
Ok(FileTransferResult::Transferred { size: local_size })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronisiert eine Datei aus dem Tresor auf die lokale Festplatte (Pull).
|
||||||
|
pub fn sync_single_file_to_host(
|
||||||
|
db: &Database,
|
||||||
|
_vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
node: &NodeRecord,
|
||||||
|
local_path: &Path,
|
||||||
|
checksum: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<FileTransferResult> {
|
||||||
|
if local_path.exists() {
|
||||||
|
if let Ok(meta) = fs::metadata(local_path) {
|
||||||
|
let local_size = meta.len();
|
||||||
|
let local_mtime = meta
|
||||||
|
.modified()
|
||||||
|
.unwrap_or(SystemTime::now())
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
if !checksum && local_size == node.size && local_mtime == node.modified_at {
|
||||||
|
return Ok(FileTransferResult::Skipped { size: node.size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
return Ok(FileTransferResult::DryRunTransferred { size: node.size });
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = local_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out_file = File::create(local_path)
|
||||||
|
.with_context(|| format!("Konnte Zieldatei '{}' nicht erstellen", local_path.display()))?;
|
||||||
|
|
||||||
|
let total_chunks = if node.size == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
((node.size - 1) / CHUNK_SIZE as u64 + 1) as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
for idx in 0..total_chunks {
|
||||||
|
if let Some(record) = db.read_chunk(node.id, idx)? {
|
||||||
|
let plaintext = decrypt_chunk(dek, node.id, idx, &record.ciphertext, &record.nonce, &record.tag, version)?;
|
||||||
|
out_file.write_all(&plaintext)?;
|
||||||
|
} else {
|
||||||
|
bail!("Beschädigte Datei im Tresor: Chunk #{} für Knoten '{}' fehlt", idx, node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out_file.flush()?;
|
||||||
|
set_local_file_mtime(&out_file, node.modified_at);
|
||||||
|
|
||||||
|
Ok(FileTransferResult::Transferred { size: node.size })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt die vollständige Synchronisation zwischen Host und Container aus.
|
||||||
|
pub fn run_sync(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
source_arg: &str,
|
||||||
|
target_arg: &str,
|
||||||
|
options: &SyncOptions,
|
||||||
|
) -> Result<SyncStats> {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
let mut stats = SyncStats::default();
|
||||||
|
|
||||||
|
match options.direction {
|
||||||
|
SyncDirection::Push => {
|
||||||
|
sync_push(db, vault_id, dek, version, source_arg, target_arg, options, &mut stats)?;
|
||||||
|
}
|
||||||
|
SyncDirection::Pull => {
|
||||||
|
sync_pull(db, vault_id, dek, version, source_arg, target_arg, options, &mut stats)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.elapsed = start_time.elapsed();
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push: Host -> Tresor
|
||||||
|
fn sync_push(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
source_str: &str,
|
||||||
|
target_str: &str,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
let local_source = Path::new(source_str);
|
||||||
|
if !local_source.exists() {
|
||||||
|
bail!("Lokale Quelle '{}' existiert nicht.", source_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_vault_dir = if target_str.is_empty() { "/" } else { target_str };
|
||||||
|
|
||||||
|
if local_source.is_file() {
|
||||||
|
let file_name = local_source
|
||||||
|
.file_name()
|
||||||
|
.context("Ungültiger Dateiname")?
|
||||||
|
.to_string_lossy();
|
||||||
|
|
||||||
|
if is_excluded(&file_name, &file_name, &options.exclude_patterns) {
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Ausgeschlossen: {}", ui::dim("[-]"), file_name);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_node = ensure_vault_dir_tree(db, vault_id, dek, target_vault_dir)?;
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
|
||||||
|
match sync_single_file_to_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
local_source,
|
||||||
|
parent_node.id,
|
||||||
|
&file_name,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Übertragen: {} ({})", ui::green("[+]"), file_name, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde übertragen: {} ({})", ui::yellow("[~]"), file_name, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), file_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if local_source.is_dir() {
|
||||||
|
let parent_node = ensure_vault_dir_tree(db, vault_id, dek, target_vault_dir)?;
|
||||||
|
let mut local_relative_paths = HashSet::new();
|
||||||
|
|
||||||
|
// Rekursiv alle lokalen Dateien und Ordner erfassen
|
||||||
|
collect_and_push_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
local_source,
|
||||||
|
local_source,
|
||||||
|
parent_node.id,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
&mut local_relative_paths,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Spiegelung mit --delete: Im Tresor verwaiste Dateien entfernen
|
||||||
|
if options.delete {
|
||||||
|
delete_orphans_in_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
parent_node.id,
|
||||||
|
"",
|
||||||
|
&local_relative_paths,
|
||||||
|
options.dry_run,
|
||||||
|
options.quiet,
|
||||||
|
stats,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_and_push_dir(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
base_dir: &Path,
|
||||||
|
current_dir: &Path,
|
||||||
|
current_vault_parent_id: i64,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
local_relative_paths: &mut HashSet<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
for entry in fs::read_dir(current_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let rel_path = path
|
||||||
|
.strip_prefix(base_dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
|
||||||
|
if is_excluded(&file_name, &rel_path, &options.exclude_patterns) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_relative_paths.insert(rel_path.clone());
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
// Ordner im Tresor anlegen falls nötig
|
||||||
|
let children = db.list_children_in_vault(current_vault_parent_id, vault_id, dek)?;
|
||||||
|
let sub_dir_node = match children.into_iter().find(|c| c.name == file_name && c.is_dir) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
if !options.dry_run {
|
||||||
|
db.create_node_in_vault(vault_id, current_vault_parent_id, &file_name, true, dek)?
|
||||||
|
} else {
|
||||||
|
// Dummy für dry-run
|
||||||
|
NodeRecord {
|
||||||
|
id: -1,
|
||||||
|
parent_id: Some(current_vault_parent_id),
|
||||||
|
name: file_name.clone(),
|
||||||
|
is_dir: true,
|
||||||
|
size: 0,
|
||||||
|
created_at: 0,
|
||||||
|
modified_at: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
collect_and_push_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
base_dir,
|
||||||
|
&path,
|
||||||
|
sub_dir_node.id,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
local_relative_paths,
|
||||||
|
)?;
|
||||||
|
} else if path.is_file() {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
match sync_single_file_to_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
&path,
|
||||||
|
current_vault_parent_id,
|
||||||
|
&file_name,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Übertragen: {} ({})", ui::green("[+]"), rel_path, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde übertragen: {} ({})", ui::yellow("[~]"), rel_path, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), rel_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_orphans_in_vault(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
current_vault_id: i64,
|
||||||
|
prefix_rel: &str,
|
||||||
|
local_relative_paths: &HashSet<String>,
|
||||||
|
dry_run: bool,
|
||||||
|
quiet: bool,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
let children = db.list_children_in_vault(current_vault_id, vault_id, dek)?;
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
let child_rel = if prefix_rel.is_empty() {
|
||||||
|
child.name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", prefix_rel, child.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !local_relative_paths.contains(&child_rel) {
|
||||||
|
stats.files_deleted += 1;
|
||||||
|
if dry_run {
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde aus Tresor löschen: {}", ui::red("[-]"), child_rel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
db.delete_node(child.id)?;
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} Gelöscht aus Tresor: {}", ui::red("[-]"), child_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if child.is_dir {
|
||||||
|
delete_orphans_in_vault(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
child.id,
|
||||||
|
&child_rel,
|
||||||
|
local_relative_paths,
|
||||||
|
dry_run,
|
||||||
|
quiet,
|
||||||
|
stats,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull: Tresor -> Host
|
||||||
|
fn sync_pull(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
source_vault_str: &str,
|
||||||
|
target_host_str: &str,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
let vault_source_node = db
|
||||||
|
.resolve_path_in_vault(source_vault_str, vault_id, dek)?
|
||||||
|
.with_context(|| format!("Quelle '{}' im Tresor nicht gefunden", source_vault_str))?;
|
||||||
|
|
||||||
|
let local_target_dir = Path::new(target_host_str);
|
||||||
|
|
||||||
|
if !vault_source_node.is_dir {
|
||||||
|
// Einzelne Datei aus Tresor laden
|
||||||
|
let local_file_path = if local_target_dir.is_dir() {
|
||||||
|
local_target_dir.join(&vault_source_node.name)
|
||||||
|
} else {
|
||||||
|
local_target_dir.to_path_buf()
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
match sync_single_file_to_host(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
&vault_source_node,
|
||||||
|
&local_file_path,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Wiederhergestellt: {} ({})", ui::green("[+]"), local_file_path.display(), ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde wiederherstellen: {} ({})", ui::yellow("[~]"), local_file_path.display(), ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), local_file_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ganzes Verzeichnis aus Tresor laden
|
||||||
|
if !options.dry_run {
|
||||||
|
fs::create_dir_all(local_target_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut vault_relative_paths = HashSet::new();
|
||||||
|
|
||||||
|
collect_and_pull_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
vault_source_node.id,
|
||||||
|
"",
|
||||||
|
local_target_dir,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
&mut vault_relative_paths,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Spiegelung mit --delete: Lokale verwaiste Dateien entfernen
|
||||||
|
if options.delete && local_target_dir.exists() {
|
||||||
|
delete_orphans_on_host(
|
||||||
|
local_target_dir,
|
||||||
|
local_target_dir,
|
||||||
|
&vault_relative_paths,
|
||||||
|
options.dry_run,
|
||||||
|
options.quiet,
|
||||||
|
stats,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_and_pull_dir(
|
||||||
|
db: &Database,
|
||||||
|
vault_id: u32,
|
||||||
|
dek: &[u8; 32],
|
||||||
|
version: u32,
|
||||||
|
vault_node_id: i64,
|
||||||
|
rel_prefix: &str,
|
||||||
|
local_target_base: &Path,
|
||||||
|
options: &SyncOptions,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
vault_relative_paths: &mut HashSet<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let children = db.list_children_in_vault(vault_node_id, vault_id, dek)?;
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
let child_rel = if rel_prefix.is_empty() {
|
||||||
|
child.name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", rel_prefix, child.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_excluded(&child.name, &child_rel, &options.exclude_patterns) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
vault_relative_paths.insert(child_rel.clone());
|
||||||
|
let local_child_path = local_target_base.join(&child_rel.replace('/', "\\"));
|
||||||
|
|
||||||
|
if child.is_dir {
|
||||||
|
if !options.dry_run {
|
||||||
|
fs::create_dir_all(&local_child_path)?;
|
||||||
|
}
|
||||||
|
collect_and_pull_dir(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
child.id,
|
||||||
|
&child_rel,
|
||||||
|
local_target_base,
|
||||||
|
options,
|
||||||
|
stats,
|
||||||
|
vault_relative_paths,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
match sync_single_file_to_host(
|
||||||
|
db,
|
||||||
|
vault_id,
|
||||||
|
dek,
|
||||||
|
version,
|
||||||
|
&child,
|
||||||
|
&local_child_path,
|
||||||
|
options.checksum,
|
||||||
|
options.dry_run,
|
||||||
|
)? {
|
||||||
|
FileTransferResult::Transferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Wiederhergestellt: {} ({})", ui::green("[+]"), child_rel, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
|
stats.files_transferred += 1;
|
||||||
|
stats.bytes_transferred += size;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde wiederherstellen: {} ({})", ui::yellow("[~]"), child_rel, ui::format_bytes(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileTransferResult::Skipped { .. } => {
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), child_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_orphans_on_host(
|
||||||
|
base_dir: &Path,
|
||||||
|
current_dir: &Path,
|
||||||
|
vault_relative_paths: &HashSet<String>,
|
||||||
|
dry_run: bool,
|
||||||
|
quiet: bool,
|
||||||
|
stats: &mut SyncStats,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !current_dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(current_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let rel_path = path
|
||||||
|
.strip_prefix(base_dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
|
||||||
|
if !vault_relative_paths.contains(&rel_path) {
|
||||||
|
stats.files_deleted += 1;
|
||||||
|
if path.is_dir() {
|
||||||
|
if dry_run {
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde lokalen Ordner löschen: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::remove_dir_all(&path)?;
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} Lokalen Ordner gelöscht: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dry_run {
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} [DRY-RUN] Würde lokale Datei löschen: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::remove_file(&path)?;
|
||||||
|
if !quiet {
|
||||||
|
println!(" {} Lokale Datei gelöscht: {}", ui::red("[-]"), rel_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if path.is_dir() {
|
||||||
|
delete_orphans_on_host(base_dir, &path, vault_relative_paths, dry_run, quiet, stats)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_excluded_patterns() {
|
||||||
|
let patterns = vec![
|
||||||
|
"*.tmp".to_string(),
|
||||||
|
"*.crdownload".to_string(),
|
||||||
|
"Thumbs.db".to_string(),
|
||||||
|
"backup_*".to_string(),
|
||||||
|
"/logs".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(is_excluded("file.tmp", "sub/file.tmp", &patterns));
|
||||||
|
assert!(is_excluded("download.crdownload", "download.crdownload", &patterns));
|
||||||
|
assert!(is_excluded("Thumbs.db", "Thumbs.db", &patterns));
|
||||||
|
assert!(is_excluded("backup_2026.tar", "backup_2026.tar", &patterns));
|
||||||
|
assert!(!is_excluded("important.doc", "sub/important.doc", &patterns));
|
||||||
|
assert!(!is_excluded("video.mp4", "video.mp4", &patterns));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,27 @@ pub fn magenta(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatiert eine Byte-Anzahl lesbar in B, KB, MB, GB oder TB.
|
||||||
|
pub fn format_bytes(bytes: u64) -> String {
|
||||||
|
const KB: f64 = 1024.0;
|
||||||
|
const MB: f64 = KB * 1024.0;
|
||||||
|
const GB: f64 = MB * 1024.0;
|
||||||
|
const TB: f64 = GB * 1024.0;
|
||||||
|
|
||||||
|
let b = bytes as f64;
|
||||||
|
if b >= TB {
|
||||||
|
format!("{:.2} TB", b / TB)
|
||||||
|
} else if b >= GB {
|
||||||
|
format!("{:.2} GB", b / GB)
|
||||||
|
} else if b >= MB {
|
||||||
|
format!("{:.2} MB", b / MB)
|
||||||
|
} else if b >= KB {
|
||||||
|
format!("{:.2} KB", b / KB)
|
||||||
|
} else {
|
||||||
|
format!("{} B", bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
||||||
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
||||||
let tag = cyan(&format!("[{}/{}]", num, total));
|
let tag = cyan(&format!("[{}/{}]", num, total));
|
||||||
|
|||||||
+454
@@ -0,0 +1,454 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::ui;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub const TARGET_ASSET_NAME: &str = "sanctum.exe";
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub const TARGET_ASSET_NAME: &str = "sanctum";
|
||||||
|
|
||||||
|
pub const CHECKSUM_ASSET_NAME: &str = "SHA256SUMS.txt";
|
||||||
|
|
||||||
|
/// Repräsentiert ein Asset in einem Gitea-Release
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ReleaseAsset {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repräsentiert ein Release von der Gitea Releases-API
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct GiteaRelease {
|
||||||
|
pub id: u64,
|
||||||
|
pub tag_name: String,
|
||||||
|
pub name: String,
|
||||||
|
pub body: String,
|
||||||
|
pub html_url: String,
|
||||||
|
pub published_at: Option<String>,
|
||||||
|
pub assets: Vec<ReleaseAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optionen für das Upgrade
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpgradeOptions {
|
||||||
|
pub check_only: bool,
|
||||||
|
pub yes: bool,
|
||||||
|
pub force: bool,
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UpgradeOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
check_only: false,
|
||||||
|
yes: false,
|
||||||
|
force: false,
|
||||||
|
base_url: "https://gitea.pansi.eu".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erkannter Paketmanager
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PackageManager {
|
||||||
|
Scoop,
|
||||||
|
Winget,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageManager {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PackageManager::Scoop => "Scoop",
|
||||||
|
PackageManager::Winget => "Winget",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upgrade_command(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PackageManager::Scoop => "scoop update sanctum",
|
||||||
|
PackageManager::Winget => "winget upgrade HaraldPansi.Sanctum",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft, ob die laufende Binary durch einen Paketmanager (Scoop oder Winget) verwaltet wird.
|
||||||
|
pub fn detect_package_manager() -> Option<PackageManager> {
|
||||||
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
|
detect_package_manager_from_path(&exe_path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hilfsfunktion für die Paketmanager-Erkennung anhand eines Pfads (für Tests isolierbar).
|
||||||
|
pub fn detect_package_manager_from_path(path: &Path) -> Option<PackageManager> {
|
||||||
|
let s = path.to_string_lossy().to_lowercase();
|
||||||
|
if s.contains("scoop\\apps\\sanctum")
|
||||||
|
|| s.contains("scoop/apps/sanctum")
|
||||||
|
|| s.contains("scoop\\shims")
|
||||||
|
|| s.contains("scoop/shims")
|
||||||
|
{
|
||||||
|
return Some(PackageManager::Scoop);
|
||||||
|
}
|
||||||
|
if s.contains("winget\\packages")
|
||||||
|
|| s.contains("winget/packages")
|
||||||
|
|| s.contains("winget\\links")
|
||||||
|
|| s.contains("winget/links")
|
||||||
|
{
|
||||||
|
return Some(PackageManager::Winget);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ruft das neueste Release von der Gitea Releases-API ab.
|
||||||
|
pub fn fetch_latest_release(base_url: &str) -> Result<GiteaRelease> {
|
||||||
|
let base = base_url.trim_end_matches('/');
|
||||||
|
let api_url = format!("{}/api/v1/repos/harald/sanctum/releases/latest", base);
|
||||||
|
|
||||||
|
let resp = ureq::get(&api_url)
|
||||||
|
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.set("Accept", "application/json")
|
||||||
|
.timeout(Duration::from_secs(15))
|
||||||
|
.call()
|
||||||
|
.with_context(|| format!("Netzwerkfehler beim Abrufen der Releases von '{}'", api_url))?;
|
||||||
|
|
||||||
|
let release: GiteaRelease = resp
|
||||||
|
.into_json()
|
||||||
|
.context("Ungültiges JSON-Format von der Gitea Releases-API empfangen")?;
|
||||||
|
|
||||||
|
Ok(release)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt eine Datei von einer URL als Text herunter.
|
||||||
|
pub fn fetch_text(url: &str) -> Result<String> {
|
||||||
|
let resp = ureq::get(url)
|
||||||
|
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.timeout(Duration::from_secs(15))
|
||||||
|
.call()
|
||||||
|
.with_context(|| format!("Fehler beim Herunterladen von '{}'", url))?;
|
||||||
|
|
||||||
|
let text = resp
|
||||||
|
.into_string()
|
||||||
|
.with_context(|| format!("Fehler beim Lesen des Texts von '{}'", url))?;
|
||||||
|
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrahiert die SHA-256 Prüfsumme für ein bestimmtes Asset aus dem Inhalt von `SHA256SUMS.txt`.
|
||||||
|
pub fn parse_checksum_for_asset(sha256sums_content: &str, target_asset: &str) -> Option<String> {
|
||||||
|
for line in sha256sums_content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = trimmed.split_whitespace().collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
let hash = parts[0];
|
||||||
|
let filename = parts[1].trim_start_matches('*');
|
||||||
|
if filename.eq_ignore_ascii_case(target_asset) && hash.len() == 64 {
|
||||||
|
return Some(hash.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bereinigt einen Versionsstring von führenden 'v' / 'V' Zeichen und parst ihn als SemVer.
|
||||||
|
pub fn parse_clean_version(ver_str: &str) -> Result<Version> {
|
||||||
|
let clean = ver_str.trim().trim_start_matches(|c| c == 'v' || c == 'V');
|
||||||
|
Version::parse(clean).with_context(|| format!("Ungültiges SemVer-Format: '{}'", ver_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vergleicht die aktuelle Version mit der entfernten Version.
|
||||||
|
/// Gibt `true` zurück, wenn `remote > current`.
|
||||||
|
pub fn is_newer_version(current: &str, remote: &str) -> bool {
|
||||||
|
let cur = match parse_clean_version(current) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let rem = match parse_clean_version(remote) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
rem > cur
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt das Ziel-Binary in eine temporäre Datei herunter und validiert dabei den SHA-256 Hash.
|
||||||
|
pub fn download_and_verify_binary(
|
||||||
|
download_url: &str,
|
||||||
|
expected_hash: &str,
|
||||||
|
expected_size: u64,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
let resp = ureq::get(download_url)
|
||||||
|
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
||||||
|
.timeout(Duration::from_secs(120))
|
||||||
|
.call()
|
||||||
|
.with_context(|| format!("Fehler beim Starten des Downloads von '{}'", download_url))?;
|
||||||
|
|
||||||
|
let mut reader = resp.into_reader();
|
||||||
|
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let temp_file_path = temp_dir.join(format!(
|
||||||
|
"sanctum_update_{}_{}",
|
||||||
|
std::process::id(),
|
||||||
|
TARGET_ASSET_NAME
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut temp_file = File::create(&temp_file_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Konnte temporäre Update-Datei '{}' nicht erstellen",
|
||||||
|
temp_file_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
let mut buffer = [0u8; 64 * 1024]; // 64 KB Puffer
|
||||||
|
let mut downloaded_bytes = 0u64;
|
||||||
|
|
||||||
|
println!(" • Herunterladen von: {}", download_url);
|
||||||
|
print!(" • Fortschritt: ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let n = reader
|
||||||
|
.read(&mut buffer)
|
||||||
|
.context("Fehler beim Lesen des Download-Datenstroms")?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
temp_file
|
||||||
|
.write_all(&buffer[..n])
|
||||||
|
.context("Fehler beim Schreiben in die temporäre Datei")?;
|
||||||
|
hasher.update(&buffer[..n]);
|
||||||
|
|
||||||
|
downloaded_bytes += n as u64;
|
||||||
|
|
||||||
|
if expected_size > 0 {
|
||||||
|
let percent = (downloaded_bytes as f64 / expected_size as f64 * 100.0).min(100.0);
|
||||||
|
print!(
|
||||||
|
"\r • Fortschritt: {} / {} ({:.1}%)",
|
||||||
|
ui::format_bytes(downloaded_bytes),
|
||||||
|
ui::format_bytes(expected_size),
|
||||||
|
percent
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print!(
|
||||||
|
"\r • Fortschritt: {}",
|
||||||
|
ui::format_bytes(downloaded_bytes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
temp_file
|
||||||
|
.flush()
|
||||||
|
.context("Fehler beim Flush der temporären Datei")?;
|
||||||
|
drop(temp_file);
|
||||||
|
|
||||||
|
// SHA-256 Integritätsprüfung
|
||||||
|
let calculated_hash = hex::encode(hasher.finalize()).to_lowercase();
|
||||||
|
let expected_lower = expected_hash.to_lowercase();
|
||||||
|
|
||||||
|
if calculated_hash != expected_lower {
|
||||||
|
let _ = fs::remove_file(&temp_file_path);
|
||||||
|
bail!(
|
||||||
|
"Kryptografische Prüfsumme ungültig!\n Erwartet: {}\n Berechnet: {}\nDas Update wurde aus Sicherheitsgründen verworfen.",
|
||||||
|
expected_lower,
|
||||||
|
calculated_hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" • SHA-256: {} (verifiziert)", ui::green(&calculated_hash[..16]));
|
||||||
|
|
||||||
|
Ok(temp_file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt den In-Place-Austausch des laufenden Binaries durch.
|
||||||
|
pub fn apply_upgrade(temp_binary: &Path) -> Result<PathBuf> {
|
||||||
|
let current_exe = std::env::current_exe()
|
||||||
|
.context("Konnte den Pfad der aktuell ausgeführten Datei nicht ermitteln")?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(temp_binary)
|
||||||
|
.context("Konnte Metadaten der Update-Datei nicht lesen")?
|
||||||
|
.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(temp_binary, perms)
|
||||||
|
.context("Konnte Ausführungsrechte (chmod +x / 0755) nicht setzen")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self_replace::self_replace(temp_binary)
|
||||||
|
.context("In-Place-Ersetzung der laufenden Anwendungsdatei fehlgeschlagen")?;
|
||||||
|
|
||||||
|
let _ = fs::remove_file(temp_binary);
|
||||||
|
|
||||||
|
Ok(current_exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt den gesamten Upgrade-Workflow aus.
|
||||||
|
pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
||||||
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ Sanctum — In-Place Upgrade & Aktualisierungsdienst │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Aktuelle Version: {}", ui::cyan(&format!("v{}", current_version)));
|
||||||
|
println!(" • Server: {}", options.base_url);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// 1. Paketmanager-Prüfung
|
||||||
|
if let Some(pm) = detect_package_manager() {
|
||||||
|
println!(
|
||||||
|
" {} Sanctum wird anscheinend über {} verwaltet!",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
pm.name()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Empfohlener Aktualisierungsbefehl: {}",
|
||||||
|
ui::cyan(pm.upgrade_command())
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if !options.force && !options.yes && !options.check_only {
|
||||||
|
print!(" Möchten Sie das direkte In-Place-Upgrade trotzdem fortsetzen? [j/N]: ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
let mut answer = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut answer)
|
||||||
|
.context("Fehler beim Einlesen der Benutzereingabe")?;
|
||||||
|
let trimmed = answer.trim().to_lowercase();
|
||||||
|
if trimmed != "j" && trimmed != "ja" && trimmed != "y" && trimmed != "yes" {
|
||||||
|
println!(" Upgrade abgebrochen.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Neuestes Release abfragen
|
||||||
|
println!(" {} Suche nach neuesten Releases auf Gitea ...", ui::dim("[-]"));
|
||||||
|
let release = fetch_latest_release(&options.base_url)?;
|
||||||
|
let remote_tag = &release.tag_name;
|
||||||
|
let is_newer = is_newer_version(current_version, remote_tag);
|
||||||
|
|
||||||
|
println!(" • Neueste Version: {}", if is_newer { ui::green(remote_tag) } else { ui::cyan(remote_tag) });
|
||||||
|
println!(" • Release-Name: {}", release.name);
|
||||||
|
if let Some(ref pub_at) = release.published_at {
|
||||||
|
println!(" • Veröffentlicht: {}", pub_at);
|
||||||
|
}
|
||||||
|
println!(" • Release-URL: {}", release.html_url);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if !is_newer && !options.force {
|
||||||
|
println!(
|
||||||
|
" {} Sie verwenden bereits die neueste Version von Sanctum.",
|
||||||
|
ui::green("✔")
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.check_only {
|
||||||
|
if is_newer {
|
||||||
|
println!(
|
||||||
|
" {} Ein Update auf {} ist verfügbar! Führen Sie 'sanctum upgrade' aus.",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
remote_tag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Ziel-Asset und Checksumme identifizieren
|
||||||
|
let target_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.eq_ignore_ascii_case(TARGET_ASSET_NAME))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Im Release '{}' wurde kein passendes Binary für Ihr Betriebssystem ('{}') gefunden!",
|
||||||
|
remote_tag,
|
||||||
|
TARGET_ASSET_NAME
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let checksum_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.eq_ignore_ascii_case(CHECKSUM_ASSET_NAME))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Im Release '{}' wurde keine Prüfsummendatei ('{}') gefunden!",
|
||||||
|
remote_tag,
|
||||||
|
CHECKSUM_ASSET_NAME
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 4. Prüfsummen herunterladen und extrahieren
|
||||||
|
println!(" {} Lade Prüfsummen ({}) herunter ...", ui::dim("[-]"), CHECKSUM_ASSET_NAME);
|
||||||
|
let sums_content = fetch_text(&checksum_asset.browser_download_url)?;
|
||||||
|
let expected_hash = parse_checksum_for_asset(&sums_content, TARGET_ASSET_NAME).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Prüfsummendatei enthält keinen SHA-256 Eintrag für '{}'!",
|
||||||
|
TARGET_ASSET_NAME
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 5. Interaktive Bestätigung (sofern nicht -y / --yes)
|
||||||
|
if !options.yes {
|
||||||
|
println!(" • Ziel-Binary: {} ({})", target_asset.name, ui::format_bytes(target_asset.size));
|
||||||
|
println!(" • Erwarteter Hash: {}...", &expected_hash[..16]);
|
||||||
|
println!();
|
||||||
|
print!(
|
||||||
|
" Möchten Sie Sanctum jetzt von v{} auf {} aktualisieren? [J/n]: ",
|
||||||
|
current_version, remote_tag
|
||||||
|
);
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
let mut answer = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut answer)
|
||||||
|
.context("Fehler beim Einlesen der Benutzereingabe")?;
|
||||||
|
let trimmed = answer.trim().to_lowercase();
|
||||||
|
if trimmed == "n" || trimmed == "nein" || trimmed == "no" {
|
||||||
|
println!(" Upgrade abgebrochen.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Download & Verifikation
|
||||||
|
println!(" {} Lade neues Binary herunter ...", ui::dim("[-]"));
|
||||||
|
let temp_file = download_and_verify_binary(
|
||||||
|
&target_asset.browser_download_url,
|
||||||
|
&expected_hash,
|
||||||
|
target_asset.size,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 7. In-Place-Austausch
|
||||||
|
println!(" {} Führe In-Place-Austausch durch ...", ui::dim("[-]"));
|
||||||
|
let updated_exe = apply_upgrade(&temp_file)?;
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ✔ Sanctum wurde erfolgreich auf {} aktualisiert! │", remote_tag);
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" • Pfad: {}", updated_exe.display());
|
||||||
|
println!(" • Neue Version: {}", ui::green(remote_tag));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+198
-29
@@ -168,8 +168,15 @@ impl SanctumFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Schreibt den aktuell im RAM gehaltenen Chunk verschlüsselt in die SQLite-Datenbank zurück.
|
/// Schreibt den aktuell im RAM gehaltenen Chunk verschlüsselt in die SQLite-Datenbank zurück
|
||||||
fn flush_cached_chunk(&mut self) -> Result<(), FsError> {
|
/// und aktualisiert Dateigröße und Modifikationszeitstempel atomar in einer Transaktion (CHAOS-01).
|
||||||
|
/// Bei Fehlern (z. B. Disk Full) wird der Cache sauber invalidiert (CHAOS-03).
|
||||||
|
fn flush_cached_chunk_and_size(&mut self) -> Result<(), FsError> {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
if let Some((idx, ref data, true)) = self.cached_chunk {
|
if let Some((idx, ref data, true)) = self.cached_chunk {
|
||||||
let (ciphertext, nonce, tag) =
|
let (ciphertext, nonce, tag) =
|
||||||
encrypt_chunk(&self.dek, self.node_id, idx, data, self.format_version).map_err(|e| {
|
encrypt_chunk(&self.dek, self.node_id, idx, data, self.format_version).map_err(|e| {
|
||||||
@@ -177,16 +184,35 @@ impl SanctumFile {
|
|||||||
FsError::GeneralFailure
|
FsError::GeneralFailure
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.db
|
if let Err(e) = self.db.write_chunk_and_update_size(
|
||||||
.write_chunk(self.node_id, idx, &nonce, &tag, &ciphertext)
|
self.node_id,
|
||||||
.map_err(|e| {
|
idx,
|
||||||
error!("DB-Fehler beim Schreiben des Chunks: {e}");
|
&nonce,
|
||||||
FsError::GeneralFailure
|
&tag,
|
||||||
})?;
|
&ciphertext,
|
||||||
|
self.file_size,
|
||||||
|
now,
|
||||||
|
) {
|
||||||
|
error!("DB-Fehler beim atomaren Chunk- und Size-Write #{idx}: {e}");
|
||||||
|
// CHAOS-03: Bei I/O- oder Disk-Full-Fehlern den Cache sauber invalidieren,
|
||||||
|
// um Folgefehler und Panic-/Warnungsschleifen beim Drop zu unterbinden!
|
||||||
|
self.cached_chunk = None;
|
||||||
|
return Err(FsError::GeneralFailure);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((_, _, ref mut dirty)) = self.cached_chunk {
|
if let Some((_, _, ref mut dirty)) = self.cached_chunk {
|
||||||
*dirty = false;
|
*dirty = false;
|
||||||
}
|
}
|
||||||
|
self.meta.size = self.file_size;
|
||||||
|
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||||
|
} else if self.meta.size != self.file_size {
|
||||||
|
// Falls kein Chunk dirty war, aber sich z. B. die Dateigröße durch Truncate geändert hat
|
||||||
|
if let Err(e) = self.db.update_node_size_and_time(self.node_id, self.file_size, now) {
|
||||||
|
error!("Fehler beim Aktualisieren der Knotengröße: {e}");
|
||||||
|
return Err(FsError::GeneralFailure);
|
||||||
|
}
|
||||||
|
self.meta.size = self.file_size;
|
||||||
|
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -199,7 +225,7 @@ impl SanctumFile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !is_current {
|
if !is_current {
|
||||||
self.flush_cached_chunk()?;
|
self.flush_cached_chunk_and_size()?;
|
||||||
if let Some((_, ref mut data, _)) = self.cached_chunk {
|
if let Some((_, ref mut data, _)) = self.cached_chunk {
|
||||||
data.zeroize();
|
data.zeroize();
|
||||||
}
|
}
|
||||||
@@ -236,17 +262,12 @@ impl SanctumFile {
|
|||||||
|
|
||||||
impl Drop for SanctumFile {
|
impl Drop for SanctumFile {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Err(e) = self.flush_cached_chunk() {
|
if let Err(e) = self.flush_cached_chunk_and_size() {
|
||||||
warn!("Fehler beim automatischen Flush im SanctumFile::drop: {:?}", e);
|
warn!("Fehler beim automatischen Flush im SanctumFile::drop: {:?}", e);
|
||||||
}
|
}
|
||||||
if let Some((_, ref mut data, _)) = self.cached_chunk {
|
if let Some((_, ref mut data, _)) = self.cached_chunk {
|
||||||
data.zeroize();
|
data.zeroize();
|
||||||
}
|
}
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|d| d.as_secs())
|
|
||||||
.unwrap_or(0);
|
|
||||||
let _ = self.db.update_node_size_and_time(self.node_id, self.file_size, now);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +355,7 @@ impl DavFile for SanctumFile {
|
|||||||
|
|
||||||
// Wenn der Chunk exakt 1 MB erreicht hat, sofort flushen, um RAM zu schonen
|
// Wenn der Chunk exakt 1 MB erreicht hat, sofort flushen, um RAM zu schonen
|
||||||
if self.cached_chunk.as_ref().map(|(_, d, _)| d.len() >= CHUNK_SIZE).unwrap_or(false) {
|
if self.cached_chunk.as_ref().map(|(_, d, _)| d.len() >= CHUNK_SIZE).unwrap_or(false) {
|
||||||
self.flush_cached_chunk()?;
|
self.flush_cached_chunk_and_size()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
src = &src[to_write..];
|
src = &src[to_write..];
|
||||||
@@ -370,19 +391,7 @@ impl DavFile for SanctumFile {
|
|||||||
fn flush(&mut self) -> FsFuture<'_, ()> {
|
fn flush(&mut self) -> FsFuture<'_, ()> {
|
||||||
self.touch();
|
self.touch();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.flush_cached_chunk()?;
|
self.flush_cached_chunk_and_size()?;
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|d| d.as_secs())
|
|
||||||
.unwrap_or(0);
|
|
||||||
self.db
|
|
||||||
.update_node_size_and_time(self.node_id, self.file_size, now)
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("Fehler beim Aktualisieren der Knotengröße: {e}");
|
|
||||||
FsError::GeneralFailure
|
|
||||||
})?;
|
|
||||||
self.meta.size = self.file_size;
|
|
||||||
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -446,6 +455,16 @@ impl SanctumFs {
|
|||||||
let dek_arc = Arc::new(dek);
|
let dek_arc = Arc::new(dek);
|
||||||
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
||||||
|
|
||||||
|
// Im Decoy-Vault (Slot 0): Stelle sicher, dass carrier_node_id stets bekannt ist,
|
||||||
|
// um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen.
|
||||||
|
let carrier_node_id = carrier_node_id.or_else(|| {
|
||||||
|
if vault_id == 0 {
|
||||||
|
db.find_carrier_node_id().ok().flatten()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let carrier_fs = if vault_id == 1 {
|
let carrier_fs = if vault_id == 1 {
|
||||||
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_arc, carrier_node_id) {
|
if let (Some(ref c_dek), Some(c_nid)) = (&carrier_dek_arc, carrier_node_id) {
|
||||||
match CarrierFs::load(
|
match CarrierFs::load(
|
||||||
@@ -529,6 +548,7 @@ impl SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_path(&self, path: &str) -> Result<Option<NodeRecord>, FsError> {
|
fn resolve_path(&self, path: &str) -> Result<Option<NodeRecord>, FsError> {
|
||||||
|
validate_path_safety(path)?;
|
||||||
self.db
|
self.db
|
||||||
.resolve_path_in_vault(path, self.vault_id, &self.dek)
|
.resolve_path_in_vault(path, self.vault_id, &self.dek)
|
||||||
.map_err(|_| FsError::GeneralFailure)
|
.map_err(|_| FsError::GeneralFailure)
|
||||||
@@ -541,6 +561,7 @@ impl SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_node(&self, parent_id: i64, name: &str, is_dir: bool) -> Result<NodeRecord, FsError> {
|
fn create_node(&self, parent_id: i64, name: &str, is_dir: bool) -> Result<NodeRecord, FsError> {
|
||||||
|
validate_path_safety(name)?;
|
||||||
self.db
|
self.db
|
||||||
.create_node_in_vault(self.vault_id, parent_id, name, is_dir, &self.dek)
|
.create_node_in_vault(self.vault_id, parent_id, name, is_dir, &self.dek)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -550,12 +571,26 @@ impl SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn rename_node(&self, id: i64, new_parent_id: i64, new_name: &str) -> Result<(), FsError> {
|
fn rename_node(&self, id: i64, new_parent_id: i64, new_name: &str) -> Result<(), FsError> {
|
||||||
|
validate_path_safety(new_name)?;
|
||||||
self.db
|
self.db
|
||||||
.rename_node_in_vault(id, new_parent_id, new_name, self.vault_id, &self.dek)
|
.rename_node_in_vault(id, new_parent_id, new_name, self.vault_id, &self.dek)
|
||||||
.map_err(|_| FsError::GeneralFailure)
|
.map_err(|_| FsError::GeneralFailure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validiert, dass ein Pfad oder Dateiname keine Null-Bytes oder unzulässige Steuerzeichen enthält (CHAOS-02).
|
||||||
|
pub fn validate_path_safety(path: &str) -> Result<(), FsError> {
|
||||||
|
if path.contains('\0') {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
for c in path.chars() {
|
||||||
|
if (c as u32) < 0x20 && c != '\t' {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for SanctumFs {
|
impl Drop for SanctumFs {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
crate::windows::unlock_memory(self.dek.as_ptr(), 32);
|
crate::windows::unlock_memory(self.dek.as_ptr(), 32);
|
||||||
@@ -787,6 +822,13 @@ impl DavFileSystem for SanctumFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schutz der Trägerdatei im Decoy Vault: Verzeichnis darf nicht gelöscht werden, wenn es den Carrier enthält!
|
||||||
|
if let Some(carrier_id) = self.carrier_node_id {
|
||||||
|
if self.db.is_descendant_of(carrier_id, node.id).map_err(|_| FsError::GeneralFailure)? {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.db
|
self.db
|
||||||
.delete_node(node.id)
|
.delete_node(node.id)
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
@@ -866,6 +908,10 @@ impl DavFileSystem for SanctumFs {
|
|||||||
if dest.is_dir {
|
if dest.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
// Schutz der Trägerdatei im Decoy Vault: Überschreiben durch Rename verboten!
|
||||||
|
if self.carrier_node_id == Some(dest.id) {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
self.db
|
self.db
|
||||||
.delete_node(dest.id)
|
.delete_node(dest.id)
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
@@ -901,6 +947,13 @@ impl DavFileSystem for SanctumFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schutz der Trägerdatei im Decoy Vault: Überschreiben durch Copy verboten!
|
||||||
|
if let Some(dest) = self.resolve_path(&to_str)? {
|
||||||
|
if self.carrier_node_id == Some(dest.id) {
|
||||||
|
return Err(FsError::Forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let to_parent = self
|
let to_parent = self
|
||||||
.resolve_path(to_parent_path)?
|
.resolve_path(to_parent_path)?
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
@@ -1124,4 +1177,120 @@ mod tests {
|
|||||||
let new_time = act_arc.load(Ordering::Relaxed);
|
let new_time = act_arc.load(Ordering::Relaxed);
|
||||||
assert!(new_time > 1000);
|
assert!(new_time > 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_carrier_protection_in_decoy_vault() {
|
||||||
|
let (fs, _dir) = create_test_fs(true);
|
||||||
|
// Erstelle eine Carrier-Datei "system_backup.dat"
|
||||||
|
let carrier_node = fs.db.create_node(1, "system_backup.dat", false).unwrap();
|
||||||
|
let carrier_id = carrier_node.id;
|
||||||
|
|
||||||
|
// Erstelle FS mit bekanntem carrier_node_id
|
||||||
|
let protected_fs = SanctumFs::with_carrier(
|
||||||
|
fs.db.clone(),
|
||||||
|
(*fs.dek).clone(),
|
||||||
|
None,
|
||||||
|
Some(carrier_id),
|
||||||
|
FORMAT_VERSION,
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let carrier_path = DavPath::new("/system_backup.dat").unwrap();
|
||||||
|
|
||||||
|
// 1. Lesen muss erlaubt sein
|
||||||
|
let mut read_opts = OpenOptions::default();
|
||||||
|
read_opts.read = true;
|
||||||
|
assert!(protected_fs.open(&carrier_path, read_opts).await.is_ok());
|
||||||
|
|
||||||
|
// 2. Schreiben / Truncate muss verboten sein (FsError::Forbidden)
|
||||||
|
let mut write_opts = OpenOptions::default();
|
||||||
|
write_opts.write = true;
|
||||||
|
assert!(matches!(
|
||||||
|
protected_fs.open(&carrier_path, write_opts).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut trunc_opts = OpenOptions::default();
|
||||||
|
trunc_opts.truncate = true;
|
||||||
|
assert!(matches!(
|
||||||
|
protected_fs.open(&carrier_path, trunc_opts).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
));
|
||||||
|
|
||||||
|
// 3. Löschen der Carrier-Datei muss verboten sein
|
||||||
|
assert!(matches!(
|
||||||
|
protected_fs.remove_file(&carrier_path).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
));
|
||||||
|
|
||||||
|
// 4. Umbenennen der Carrier-Datei muss verboten sein
|
||||||
|
let rename_target = DavPath::new("/renamed_backup.dat").unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
protected_fs.rename(&carrier_path, &rename_target).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
));
|
||||||
|
|
||||||
|
// 5. Überschreiben der Carrier-Datei durch Rename einer anderen Datei muss verboten sein
|
||||||
|
let other_path = DavPath::new("/other.txt").unwrap();
|
||||||
|
let mut other_opts = OpenOptions::default();
|
||||||
|
other_opts.write = true;
|
||||||
|
other_opts.create_new = true;
|
||||||
|
protected_fs.open(&other_path, other_opts).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
protected_fs.rename(&other_path, &carrier_path).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
));
|
||||||
|
|
||||||
|
// 6. Überschreiben der Carrier-Datei durch Copy einer anderen Datei muss verboten sein
|
||||||
|
assert!(matches!(
|
||||||
|
protected_fs.copy(&other_path, &carrier_path).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_path_safety_rejects_null_bytes_and_control_chars() {
|
||||||
|
let (fs, _dir) = create_test_fs(true);
|
||||||
|
|
||||||
|
// 1. Null-Byte im Pfad
|
||||||
|
let null_path = DavPath::new("/bad\0file.txt");
|
||||||
|
assert!(null_path.is_err() || fs.open(&null_path.unwrap(), OpenOptions::default()).await.is_err());
|
||||||
|
assert!(validate_path_safety("/bad\0file.txt").is_err());
|
||||||
|
|
||||||
|
// 2. Steuerzeichen < 0x20
|
||||||
|
assert!(validate_path_safety("/bad\x01file.txt").is_err());
|
||||||
|
assert!(validate_path_safety("/bad\rfile.txt").is_err());
|
||||||
|
assert!(validate_path_safety("/bad\nfile.txt").is_err());
|
||||||
|
|
||||||
|
// 3. Gültiger Pfad
|
||||||
|
assert!(validate_path_safety("/normal_file_123.txt").is_ok());
|
||||||
|
assert!(validate_path_safety("/path/to/subfolder/file.pdf").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_atomic_and_cache_invalidation() {
|
||||||
|
let (fs, _dir) = create_test_fs(true);
|
||||||
|
let path = DavPath::new("/atomic_test.bin").unwrap();
|
||||||
|
|
||||||
|
let mut opts = OpenOptions::default();
|
||||||
|
opts.write = true;
|
||||||
|
opts.create_new = true;
|
||||||
|
let mut file = fs.open(&path, opts).await.unwrap();
|
||||||
|
|
||||||
|
// 1. Schreibe 500 Bytes
|
||||||
|
let payload = Bytes::from(vec![42u8; 500]);
|
||||||
|
file.write_buf(Box::new(std::io::Cursor::new(payload))).await.unwrap();
|
||||||
|
|
||||||
|
// 2. Expliziter Flush: muss Chunk & Dateigröße atomar persistieren
|
||||||
|
file.flush().await.unwrap();
|
||||||
|
|
||||||
|
let node = fs.resolve_path("/atomic_test.bin").unwrap().unwrap();
|
||||||
|
assert_eq!(node.size, 500);
|
||||||
|
|
||||||
|
// Chunk in DB prüfen
|
||||||
|
let chunk = fs.db.read_chunk(node.id, 0).unwrap().unwrap();
|
||||||
|
assert!(!chunk.ciphertext.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+212
-96
@@ -1,5 +1,6 @@
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
|
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
|
||||||
@@ -32,14 +33,68 @@ pub fn find_next_available_drive() -> Result<char> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Öffnet das eingebundene Netzlaufwerk direkt im Windows Explorer.
|
/// Öffnet das eingebundene Netzlaufwerk oder Verzeichnis direkt im systemeigenen Dateimanager
|
||||||
|
/// (Windows: Explorer, macOS: open, Linux: xdg-open).
|
||||||
pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
||||||
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
|
#[cfg(windows)]
|
||||||
Command::new("explorer.exe")
|
{
|
||||||
.arg(&drive_path)
|
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
|
||||||
.spawn()
|
Command::new("explorer.exe")
|
||||||
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
|
.arg(&drive_path)
|
||||||
Ok(())
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let _ = drive_char;
|
||||||
|
let _ = Command::new("open").arg(".").spawn();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = drive_char;
|
||||||
|
let _ = Command::new("xdg-open").arg(".").spawn();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = drive_char;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Öffnet einen beliebigen Pfad im nativen Dateimanager der Plattform.
|
||||||
|
pub fn open_in_file_manager(path: &std::path::Path) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
Command::new("explorer.exe")
|
||||||
|
.arg(path)
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
Command::new("open")
|
||||||
|
.arg(path)
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte macOS Finder für '{}' nicht öffnen", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
Command::new("xdg-open")
|
||||||
|
.arg(path)
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("Konnte Dateimanager via xdg-open für '{}' nicht öffnen", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
|
{
|
||||||
|
let _ = path;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED).
|
/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED).
|
||||||
@@ -67,104 +122,146 @@ pub fn notify_shell_associations_changed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = Command::new("update-desktop-database").spawn();
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, all(unix, not(target_os = "macos")))))]
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte).
|
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer bzw. unter Linux via Freedesktop.
|
||||||
pub fn register_explorer_integration() -> Result<()> {
|
pub fn register_explorer_integration() -> Result<()> {
|
||||||
let current_exe = std::env::current_exe()
|
#[cfg(windows)]
|
||||||
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?;
|
{
|
||||||
let exe_str = current_exe.display().to_string();
|
let current_exe = std::env::current_exe()
|
||||||
|
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?;
|
||||||
|
let exe_str = current_exe.display().to_string();
|
||||||
|
|
||||||
let reg_commands = [
|
let reg_commands = [
|
||||||
// 1. .sanctum Erweiterung mit ProgID verknüpfen
|
// 1. .sanctum Erweiterung mit ProgID verknüpfen
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\.sanctum",
|
r"HKCU\Software\Classes\.sanctum",
|
||||||
"",
|
"",
|
||||||
"Sanctum.Container",
|
"Sanctum.Container",
|
||||||
),
|
),
|
||||||
// 2. ProgID Metadaten & Beschreibung
|
// 2. ProgID Metadaten & Beschreibung
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container",
|
r"HKCU\Software\Classes\Sanctum.Container",
|
||||||
"",
|
"",
|
||||||
"Sanctum Verschlüsselter Container",
|
"Sanctum Verschlüsselter Container",
|
||||||
),
|
),
|
||||||
// 3. Icon
|
// 3. Icon
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon",
|
r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\",0"),
|
&format!("\"{exe_str}\",0"),
|
||||||
),
|
),
|
||||||
// 4. Standard-Doppelklick-Aktion: Mount
|
// 4. Standard-Doppelklick-Aktion: Mount
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\open",
|
||||||
"",
|
"",
|
||||||
"Als Laufwerk einbinden",
|
"In Sanctum öffnen",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\" mount --path \"%1\""),
|
&format!("\"{exe_str}\" mount \"%1\""),
|
||||||
),
|
),
|
||||||
// 5. Kontextmenü-Aktion: Integritätsprüfung
|
// 5. Kontextmenü-Aktion: Verify / FSCK
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
|
||||||
"",
|
"",
|
||||||
"Integrität prüfen (FSCK)",
|
"Integrität prüfen (FSCK)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\" verify --path \"%1\""),
|
&format!("cmd /k \"\"{exe_str}\" verify \"%1\"\""),
|
||||||
),
|
),
|
||||||
// 6. Kontextmenü-Aktion: Header sichern
|
// 6. Kontextmenü-Aktion: Header-Backup
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\backup",
|
||||||
"",
|
"",
|
||||||
"Header sichern",
|
"Header sichern (Disaster Recovery)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command",
|
r"HKCU\Software\Classes\Sanctum.Container\shell\backup\command",
|
||||||
"",
|
"",
|
||||||
&format!("\"{exe_str}\" backup-header --path \"%1\""),
|
&format!("cmd /k \"\"{exe_str}\" backup-header \"%1\"\""),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (key, val_name, val_data) in reg_commands {
|
for (key, val_name, val_data) in reg_commands {
|
||||||
let mut cmd = Command::new("reg");
|
let mut cmd = Command::new("reg");
|
||||||
cmd.arg("add").arg(key);
|
cmd.arg("add").arg(key);
|
||||||
if val_name.is_empty() {
|
if val_name.is_empty() {
|
||||||
cmd.arg("/ve");
|
cmd.arg("/ve");
|
||||||
} else {
|
} else {
|
||||||
cmd.arg("/v").arg(val_name);
|
cmd.arg("/v").arg(val_name);
|
||||||
|
}
|
||||||
|
cmd.arg("/d").arg(val_data).arg("/f");
|
||||||
|
|
||||||
|
let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cmd.arg("/d").arg(val_data).arg("/f");
|
|
||||||
|
|
||||||
let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?;
|
notify_shell_associations_changed();
|
||||||
if !output.status.success() {
|
Ok(())
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let home_path = std::path::PathBuf::from(home);
|
||||||
|
let apps_dir = home_path.join(".local/share/applications");
|
||||||
|
let mime_dir = home_path.join(".local/share/mime/packages");
|
||||||
|
let _ = std::fs::create_dir_all(&apps_dir);
|
||||||
|
let _ = std::fs::create_dir_all(&mime_dir);
|
||||||
|
|
||||||
notify_shell_associations_changed();
|
let desktop_content = "[Desktop Entry]\nType=Application\nName=Sanctum\nComment=Verschlüsselter Ein-Datei-Container\nExec=sanctum mount %f\nIcon=security-high\nTerminal=true\nMimeType=application/x-sanctum;\nCategories=Utility;Security;\n";
|
||||||
Ok(())
|
let _ = std::fs::write(apps_dir.join("sanctum.desktop"), desktop_content);
|
||||||
|
|
||||||
|
let mime_content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n <mime-type type=\"application/x-sanctum\">\n <comment>Sanctum Verschlüsselter Container</comment>\n <glob pattern=\"*.sanctum\"/>\n </mime-type>\n</mime-info>\n";
|
||||||
|
let _ = std::fs::write(mime_dir.join("application-x-sanctum.xml"), mime_content);
|
||||||
|
|
||||||
|
notify_shell_associations_changed();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU).
|
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU) bzw. unter Linux aus Freedesktop.
|
||||||
pub fn unregister_explorer_integration() -> Result<()> {
|
pub fn unregister_explorer_integration() -> Result<()> {
|
||||||
let keys_to_delete = [
|
#[cfg(windows)]
|
||||||
r"HKCU\Software\Classes\.sanctum",
|
{
|
||||||
r"HKCU\Software\Classes\Sanctum.Container",
|
let keys_to_delete = [
|
||||||
];
|
r"HKCU\Software\Classes\.sanctum",
|
||||||
|
r"HKCU\Software\Classes\Sanctum.Container",
|
||||||
|
];
|
||||||
|
|
||||||
for key in keys_to_delete {
|
for key in keys_to_delete {
|
||||||
let _ = Command::new("reg")
|
let _ = Command::new("reg")
|
||||||
.args(["delete", key, "/f"])
|
.args(["delete", key, "/f"])
|
||||||
.output();
|
.output();
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_shell_associations_changed();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let home_path = std::path::PathBuf::from(home);
|
||||||
|
let _ = std::fs::remove_file(home_path.join(".local/share/applications/sanctum.desktop"));
|
||||||
|
let _ = std::fs::remove_file(home_path.join(".local/share/mime/packages/application-x-sanctum.xml"));
|
||||||
|
notify_shell_associations_changed();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_shell_associations_changed();
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
|
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
|
||||||
@@ -198,10 +295,9 @@ pub fn get_default_system_icon() -> Option<tray_item::IconSource> {
|
|||||||
|
|
||||||
/// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre.
|
/// Guard zur Verwaltung des Hintergrundthreads für die Windows-Sitzungssperre.
|
||||||
/// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet.
|
/// Beim Droppen wird das Win32-Nachrichtenfenster geschlossen und der Thread sauber beendet.
|
||||||
|
#[cfg(windows)]
|
||||||
pub struct SessionLockGuard {
|
pub struct SessionLockGuard {
|
||||||
#[cfg(windows)]
|
|
||||||
hwnd: isize,
|
hwnd: isize,
|
||||||
#[cfg(windows)]
|
|
||||||
join_handle: Option<std::thread::JoinHandle<()>>,
|
join_handle: Option<std::thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +623,7 @@ pub fn start_console_ctrl_monitor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys).
|
/// Verriegelt einen Speicherbereich im physischen RAM (verhindert Paging in pagefile.sys / swapfile.sys unter Windows bzw. Swap unter Linux/macOS).
|
||||||
pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
|
pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
@@ -539,7 +635,17 @@ pub fn lock_memory(ptr: *const u8, len: usize) -> bool {
|
|||||||
}
|
}
|
||||||
unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 }
|
unsafe { VirtualLock(ptr as *const std::ffi::c_void, len) != 0 }
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
extern "C" {
|
||||||
|
fn mlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int;
|
||||||
|
}
|
||||||
|
if ptr.is_null() || len == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsafe { mlock(ptr as *const std::ffi::c_void, len) == 0 }
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
{
|
{
|
||||||
let _ = (ptr, len);
|
let _ = (ptr, len);
|
||||||
false
|
false
|
||||||
@@ -558,7 +664,17 @@ pub fn unlock_memory(ptr: *const u8, len: usize) -> bool {
|
|||||||
}
|
}
|
||||||
unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 }
|
unsafe { VirtualUnlock(ptr as *const std::ffi::c_void, len) != 0 }
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
extern "C" {
|
||||||
|
fn munlock(addr: *const std::ffi::c_void, len: usize) -> std::ffi::c_int;
|
||||||
|
}
|
||||||
|
if ptr.is_null() || len == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsafe { munlock(ptr as *const std::ffi::c_void, len) == 0 }
|
||||||
|
}
|
||||||
|
#[cfg(not(any(windows, unix)))]
|
||||||
{
|
{
|
||||||
let _ = (ptr, len);
|
let _ = (ptr, len);
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use sanctum::crypto::{
|
||||||
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION,
|
||||||
|
};
|
||||||
|
use sanctum::storage::Database;
|
||||||
|
use sanctum::sync::{run_sync, SyncDirection, SyncOptions};
|
||||||
|
|
||||||
|
fn create_test_container(path: &Path) -> (Database, [u8; 32]) {
|
||||||
|
if path.exists() {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
let db = Database::open(path).expect("Open database");
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: 1024,
|
||||||
|
time_cost: 1,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).unwrap();
|
||||||
|
(db, *dek)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_push_and_pull_basic() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
let target_dir = temp_root.join("restored");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
fs::create_dir_all(source_dir.join("sub")).unwrap();
|
||||||
|
|
||||||
|
// Testdateien anlegen
|
||||||
|
fs::write(source_dir.join("file1.txt"), b"Hello Sanctum Sync!").unwrap();
|
||||||
|
fs::write(source_dir.join("sub").join("file2.bin"), vec![0x42u8; 100_000]).unwrap();
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
// 1. Push
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Backup",
|
||||||
|
&opts,
|
||||||
|
).expect("Sync push");
|
||||||
|
|
||||||
|
assert_eq!(stats.files_scanned, 2);
|
||||||
|
assert_eq!(stats.files_transferred, 2);
|
||||||
|
assert_eq!(stats.files_skipped, 0);
|
||||||
|
|
||||||
|
// 2. Erneuter Push (Fast check / Delta): Muss 0 übertragen, 2 überspringen
|
||||||
|
let stats_delta = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Backup",
|
||||||
|
&opts,
|
||||||
|
).expect("Sync push delta");
|
||||||
|
|
||||||
|
assert_eq!(stats_delta.files_transferred, 0);
|
||||||
|
assert_eq!(stats_delta.files_skipped, 2);
|
||||||
|
|
||||||
|
// 3. Dry-Run mit neuer Datei
|
||||||
|
fs::write(source_dir.join("new_file.txt"), b"Brand new file").unwrap();
|
||||||
|
let mut dry_opts = opts.clone();
|
||||||
|
dry_opts.dry_run = true;
|
||||||
|
|
||||||
|
let stats_dry = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Backup",
|
||||||
|
&dry_opts,
|
||||||
|
).expect("Sync push dry-run");
|
||||||
|
|
||||||
|
assert_eq!(stats_dry.files_transferred, 1);
|
||||||
|
assert_eq!(stats_dry.files_skipped, 2);
|
||||||
|
|
||||||
|
// Verifizieren, dass new_file.txt im Tresor tatsächlich NICHT existiert
|
||||||
|
let node = db.resolve_path_in_vault("/Backup/new_file.txt", 0, &dek).unwrap();
|
||||||
|
assert!(node.is_none());
|
||||||
|
|
||||||
|
// 4. Pull
|
||||||
|
let mut pull_opts = SyncOptions::default();
|
||||||
|
pull_opts.direction = SyncDirection::Pull;
|
||||||
|
pull_opts.quiet = true;
|
||||||
|
|
||||||
|
let stats_pull = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
"/Backup",
|
||||||
|
target_dir.to_str().unwrap(),
|
||||||
|
&pull_opts,
|
||||||
|
).expect("Sync pull");
|
||||||
|
|
||||||
|
assert_eq!(stats_pull.files_transferred, 2);
|
||||||
|
|
||||||
|
// Inhalt vergleichen
|
||||||
|
let c1 = fs::read(target_dir.join("file1.txt")).unwrap();
|
||||||
|
assert_eq!(c1, b"Hello Sanctum Sync!");
|
||||||
|
let c2 = fs::read(target_dir.join("sub").join("file2.bin")).unwrap();
|
||||||
|
assert_eq!(c2, vec![0x42u8; 100_000]);
|
||||||
|
|
||||||
|
// Aufräumen
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_multi_megabyte_large_file() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_large_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test_large.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
let target_dir = temp_root.join("restored");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
|
||||||
|
// 3.5 MB große Datei erzeugen (geht über 4 Chunks: 0, 1, 2, 3)
|
||||||
|
let large_file_path = source_dir.join("large_payload.bin");
|
||||||
|
let mut large_file = File::create(&large_file_path).unwrap();
|
||||||
|
let chunk_sample = vec![0xA5u8; CHUNK_SIZE];
|
||||||
|
for _ in 0..3 {
|
||||||
|
large_file.write_all(&chunk_sample).unwrap();
|
||||||
|
}
|
||||||
|
large_file.write_all(&vec![0x5Au8; 512 * 1024]).unwrap(); // 3.5 MB
|
||||||
|
large_file.flush().unwrap();
|
||||||
|
drop(large_file);
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
// Push
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/LargeTest",
|
||||||
|
&opts,
|
||||||
|
).expect("Sync push large");
|
||||||
|
|
||||||
|
assert_eq!(stats.files_transferred, 1);
|
||||||
|
assert_eq!(stats.bytes_transferred, 3 * (CHUNK_SIZE as u64) + 512 * 1024);
|
||||||
|
|
||||||
|
// Pull
|
||||||
|
let mut pull_opts = SyncOptions::default();
|
||||||
|
pull_opts.direction = SyncDirection::Pull;
|
||||||
|
pull_opts.quiet = true;
|
||||||
|
|
||||||
|
run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
"/LargeTest",
|
||||||
|
target_dir.to_str().unwrap(),
|
||||||
|
&pull_opts,
|
||||||
|
).expect("Sync pull large");
|
||||||
|
|
||||||
|
let restored = fs::read(target_dir.join("large_payload.bin")).unwrap();
|
||||||
|
let original = fs::read(&large_file_path).unwrap();
|
||||||
|
assert_eq!(restored.len(), original.len());
|
||||||
|
assert_eq!(restored, original);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_delete_and_exclude_flags() {
|
||||||
|
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_flags_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test_flags.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
fs::write(source_dir.join("keep.txt"), b"Keep this").unwrap();
|
||||||
|
fs::write(source_dir.join("remove_me.txt"), b"Will be deleted later").unwrap();
|
||||||
|
fs::write(source_dir.join("ignore.tmp"), b"Temporary file").unwrap();
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.exclude_patterns = vec!["*.tmp".to_string()];
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
// 1. Initialer Push mit Exclude
|
||||||
|
let stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Files",
|
||||||
|
&opts,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stats.files_transferred, 2); // keep.txt und remove_me.txt
|
||||||
|
assert!(db.resolve_path_in_vault("/Files/ignore.tmp", 0, &dek).unwrap().is_none());
|
||||||
|
|
||||||
|
// 2. Lokale Datei löschen und Sync mit --delete ausführen
|
||||||
|
fs::remove_file(source_dir.join("remove_me.txt")).unwrap();
|
||||||
|
opts.delete = true;
|
||||||
|
|
||||||
|
let stats_del = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/Files",
|
||||||
|
&opts,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stats_del.files_deleted, 1);
|
||||||
|
assert!(db.resolve_path_in_vault("/Files/remove_me.txt", 0, &dek).unwrap().is_none());
|
||||||
|
assert!(db.resolve_path_in_vault("/Files/keep.txt", 0, &dek).unwrap().is_some());
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use sanctum::upgrade::{
|
||||||
|
detect_package_manager_from_path, is_newer_version, parse_checksum_for_asset,
|
||||||
|
parse_clean_version, GiteaRelease, PackageManager,
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_checksum_for_asset() {
|
||||||
|
let sample_checksums = r#"
|
||||||
|
# Offizielle SHA-256 Prüfsummen für Sanctum Release
|
||||||
|
2f069a8cc41f9fa6a2559e2952e5669523c21c175df15f6d80f0473bbe2fa60e sanctum-v0.5.0-windows-x86_64.zip
|
||||||
|
e0c64ecc12c7f49180487b638f1152eccaf5c90270a93880db52ca56eb1c1d63 sanctum.exe
|
||||||
|
557dd84f339336899fdd67e69d894dcd6e32f3e0bff2ae8a3057df596eddf4b4 sanctum-v0.5.0-linux-x86_64.tar.gz
|
||||||
|
2816f00cb15825d92f35f93fd667ed04d36501763a70b9399dd0ab9355c9448d *sanctum
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Windows Binary
|
||||||
|
let win_hash = parse_checksum_for_asset(sample_checksums, "sanctum.exe");
|
||||||
|
assert_eq!(
|
||||||
|
win_hash.as_deref(),
|
||||||
|
Some("e0c64ecc12c7f49180487b638f1152eccaf5c90270a93880db52ca56eb1c1d63")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linux Binary (mit führendem Asterisk im Format)
|
||||||
|
let linux_hash = parse_checksum_for_asset(sample_checksums, "sanctum");
|
||||||
|
assert_eq!(
|
||||||
|
linux_hash.as_deref(),
|
||||||
|
Some("2816f00cb15825d92f35f93fd667ed04d36501763a70b9399dd0ab9355c9448d")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nicht existentes Asset
|
||||||
|
let non_existent = parse_checksum_for_asset(sample_checksums, "nonexistent.exe");
|
||||||
|
assert_eq!(non_existent, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_semver_comparisons() {
|
||||||
|
// Normaler Versionssprung
|
||||||
|
assert!(is_newer_version("0.5.0", "0.6.0"));
|
||||||
|
assert!(is_newer_version("0.5.0", "v0.6.0"));
|
||||||
|
assert!(is_newer_version("v0.5.0", "V0.5.1"));
|
||||||
|
assert!(is_newer_version("0.5.0", "1.0.0"));
|
||||||
|
|
||||||
|
// Gleiche Version
|
||||||
|
assert!(!is_newer_version("0.6.0", "0.6.0"));
|
||||||
|
assert!(!is_newer_version("0.6.0", "v0.6.0"));
|
||||||
|
assert!(!is_newer_version("v0.6.0", "0.6.0"));
|
||||||
|
|
||||||
|
// Ältere Version
|
||||||
|
assert!(!is_newer_version("0.6.0", "0.5.0"));
|
||||||
|
assert!(!is_newer_version("0.6.0", "v0.4.1"));
|
||||||
|
|
||||||
|
// Parsing mit führenden 'v' / 'V'
|
||||||
|
let v1 = parse_clean_version("v0.6.0").expect("Parse v0.6.0");
|
||||||
|
let v2 = parse_clean_version("V1.2.3").expect("Parse V1.2.3");
|
||||||
|
assert_eq!(v1.major, 0);
|
||||||
|
assert_eq!(v1.minor, 6);
|
||||||
|
assert_eq!(v2.major, 1);
|
||||||
|
assert_eq!(v2.minor, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_package_manager_detection() {
|
||||||
|
// Scoop Pfade
|
||||||
|
let scoop_path = Path::new(r"C:\Users\harald\scoop\apps\sanctum\current\sanctum.exe");
|
||||||
|
assert_eq!(
|
||||||
|
detect_package_manager_from_path(scoop_path),
|
||||||
|
Some(PackageManager::Scoop)
|
||||||
|
);
|
||||||
|
|
||||||
|
let scoop_shim = Path::new(r"C:\Users\harald\scoop\shims\sanctum.exe");
|
||||||
|
assert_eq!(
|
||||||
|
detect_package_manager_from_path(scoop_shim),
|
||||||
|
Some(PackageManager::Scoop)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Winget Pfade
|
||||||
|
let winget_path = Path::new(
|
||||||
|
r"C:\Users\harald\AppData\Local\Microsoft\WinGet\Packages\HaraldPansi.Sanctum_Microsoft.Winget.Source_8wekyb3d8bbwe\sanctum.exe",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detect_package_manager_from_path(winget_path),
|
||||||
|
Some(PackageManager::Winget)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Standard Standalone Pfade (kein Paketmanager)
|
||||||
|
let standalone = Path::new(r"C:\Tools\sanctum\sanctum.exe");
|
||||||
|
assert_eq!(detect_package_manager_from_path(standalone), None);
|
||||||
|
|
||||||
|
let linux_usr = Path::new("/usr/local/bin/sanctum");
|
||||||
|
assert_eq!(detect_package_manager_from_path(linux_usr), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_integrity_calculation() {
|
||||||
|
let test_file = std::env::temp_dir().join(format!("sanctum_test_{}.bin", std::process::id()));
|
||||||
|
|
||||||
|
let payload = b"Sanctum Standalone Binary Payload for Integrity Testing";
|
||||||
|
{
|
||||||
|
let mut f = File::create(&test_file).expect("create file");
|
||||||
|
f.write_all(payload).expect("write");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload);
|
||||||
|
let expected = hex::encode(hasher.finalize());
|
||||||
|
|
||||||
|
// Datei erneut lesen und hashen
|
||||||
|
let mut reader = File::open(&test_file).expect("open");
|
||||||
|
let mut file_hasher = Sha256::new();
|
||||||
|
std::io::copy(&mut reader, &mut file_hasher).expect("copy");
|
||||||
|
let calculated = hex::encode(file_hasher.finalize());
|
||||||
|
|
||||||
|
assert_eq!(expected, calculated);
|
||||||
|
let _ = std::fs::remove_file(&test_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_json_deserialization_of_gitea_release() {
|
||||||
|
let gitea_json = r##"{
|
||||||
|
"id": 27,
|
||||||
|
"tag_name": "v0.5.0",
|
||||||
|
"name": "Sanctum v0.5.0 (Windows x86_64)",
|
||||||
|
"body": "Release v0.5.0 - sanctum sync",
|
||||||
|
"html_url": "https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.5.0",
|
||||||
|
"published_at": "2026-09-16T10:44:18+02:00",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"name": "sanctum.exe",
|
||||||
|
"size": 5467136,
|
||||||
|
"browser_download_url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"name": "sanctum",
|
||||||
|
"size": 5033952,
|
||||||
|
"browser_download_url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/sanctum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"name": "SHA256SUMS.txt",
|
||||||
|
"size": 563,
|
||||||
|
"browser_download_url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.5.0/SHA256SUMS.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"##;
|
||||||
|
|
||||||
|
let release: GiteaRelease = serde_json::from_str(gitea_json).expect("Deserialisierung erfolgreich");
|
||||||
|
assert_eq!(release.id, 27);
|
||||||
|
assert_eq!(release.tag_name, "v0.5.0");
|
||||||
|
assert_eq!(release.assets.len(), 3);
|
||||||
|
assert_eq!(release.assets[0].name, "sanctum.exe");
|
||||||
|
assert_eq!(release.assets[1].name, "sanctum");
|
||||||
|
assert_eq!(release.assets[2].name, "SHA256SUMS.txt");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user