5 Commits
Author SHA1 Message Date
harald 76c23dbfa3 fix(format-v3): transcript versioning in HMAC and atomic upgrade MAC (F-04, F-05)
Sanctum Release / Build & Test (Windows x86_64 & Linux musl) (push) Waiting to run
Sanctum Release / Sign & Release (push) Blocked by required conditions
2026-09-22 21:02:22 +02:00
harald 3a1a1ca6ce chore(release): update package manifests and audit docs for v0.9.4
Sanctum Release / Build & Test (Windows x86_64 & Linux musl) (push) Canceled after 0s
Sanctum Release / Sign & Release (push) Canceled after 0s
2026-09-22 16:54:57 +02:00
harald 510cb9b64e fix(format-v3): harden system binding against replay and bypass attacks (F-01, F-02, F-03)
- F-02: Require restore_nonce token and explicit confirmation (--rebuild-mac) for PendingRebuild
- F-01: Extend canonical MAC transcript to include chunk generation tuples (node_id, chunk_index, generation) and support transparent legacy migration
- F-03: Make V2-to-V3 container upgrade atomic with transactional rollback and dual-slot version update
- Bump version to 0.9.4 and update changelog and security docs
2026-09-22 16:48:12 +02:00
harald abcd85524d docs(agents): add AGENTS.md and GEMINI.md project context and conversation reference 2026-09-21 11:47:33 +02:00
harald 66aa52b8c5 chore(scripts): extract version-specific changelog section for release body 2026-09-21 11:38:38 +02:00
19 changed files with 2115 additions and 99 deletions
+98
View File
@@ -0,0 +1,98 @@
# Sanctum — Projekt-Kontext & Agenten-Leitfaden (AGENTS.md)
Dieses Dokument dient als primäres Projektgedächtnis für Antigravity-Agenten und Entwickler. Es verknüpft die historische Entwicklungshistorie, dokumentiert die Kernarchitektur, verweist auf alle Sicherheitsdokumente und definiert die verbindlichen Quality Gates.
---
## 1. Konversations-Referenz & Historie
Die gesamte Genese dieses Projekts (Audit-Behebung aller 39 Findings, Carrier V2 Paged Manifest, Disaster Recovery MAC-Fix R-NEW-1, Supply-Chain-Audits und Releases bis v0.9.3) ist in folgender Konversation protokolliert:
🔗 **[Sanctum Genese, Audit-Härtung & v0.9.3 Release-Konversation](conversation://f8a1e581-6556-4799-b012-fabcae853c09)**
*(ID: `f8a1e581-6556-4799-b012-fabcae853c09` — kann in Antigravity per Klick oder im Chat via `@Conversations` geladen werden).*
---
## 2. Projektüberblick & Kernarchitektur
**Sanctum** ist eine speichersichere, hochperformante Userland-CLI in Rust, die verschlüsselte Ein-Datei-Container (`.sanctum`) unter Windows 10/11 und Linux verwaltet — zu 100 % im Userland ohne Administratorrechte oder Kernel-Treiber (kein Dokan, kein WinFsp).
### 2.1 Kryptografische Säulen (Container-Format V3)
- **Format-Version**: Neue Container nutzen Format V3 (`format_version = 3`).
- **KDF**: Argon2id ($M=256\,\text{MiB}, T=4, P=4$, M-01) zur KEK-Ableitung mit CSPRNG-Salts (`OsRng`).
- **DEK & Wrapping**: 256-Bit DEK, gewrappt per AES-256-GCM.
- **Kanonische Metadaten-Authentifizierung (K-01)**:
- Inode-Metadaten werden mit einer HMAC-SHA-256 (`SANCTUM_META_MAC_V3`) authentifiziert.
- Validierung bei jedem `mount` (`verify_metadata_mac_status_for_slot`).
- Disaster-Recovery-Resilienz (R-NEW-1): Rebuild-Fähigkeit via `MetadataMacStatus::PendingRebuild` nach Header-Wiederherstellung.
- **Chunk-Replay-Schutz (K-02)**: 24-Byte AAD (`node_id || chunk_index || metadata_gen`).
- **Dateinamen-Verschlüsselung**: AES-256-GCM mit Bindung an `parent_id` als AAD (Swap-Schutz). Warnung bei `--legacy-names`.
### 2.2 Dual-Vault & Carrier-Format V2 (Paged Manifest)
- **2-Slot-Architektur**: Slot 0 als Decoy-Vault, Slot 1 als Second Safe (Hidden Vault Modell A).
- **Steganografie & Größeninvarianz**: Der Hidden Vault liegt innerhalb einer Alibi-Trägerdatei im Decoy-Vault. Versteckte Schreibvorgänge verändern die Host-Dateigröße um exakt 0 Bytes.
- **Paged Manifest V2**:
- Trennung von Superblock (redundant auf Block 0 und Block 1, rollierende `manifest_generation`, C-02) und Inode-Seiten (`CarrierInodePage`, ~1 MB Payload, ca. 4.5007.000 Inodes/Seite).
- Keine starre Kapazitätsobergrenze mehr; dynamische Skalierung mit den Trägerblöcken.
- Fail-Soft Resilienz (D-01): Isolierung beschädigter Seiten ohne Mount-Abbruch.
- In-Memory Sekundärindex (D-02): $O(\text{Geschwister})$ Pfadauflösung.
- Manifest-Entkopplung via Dirty-Tracking (C-03): Schnelle I/O-Operationen ohne synchrone 1-MB-Manifest-Neuverschlüsselung.
### 2.3 OpSec, Anti-Leak & Disaster Recovery
- **Anti-Leak Shield**: Blockiert Explorer-Artefakte (`Thumbs.db`, `desktop.ini`, `*.tmp`, NTFS ADS `:Zone.Identifier`).
- **Memory Security**: `Zeroize` für alle Schlüssel und Passwörter; `VirtualLock` / `mlock` gegen Swap-Auslagerung.
- **Transaktionales Chunk-Shredding**: Logisches Überschreiben von Chunks mit CSPRNG-Rauschen vor dem Löschen.
- **Disaster Recovery**: 24-Wort BIP-39 Notfallschlüssel pro Slot, Online-Backups via SQLite Online Backup API.
---
## 3. Zentrale Dokumentation im Repository
| Dokument | Zweck / Inhalt |
|---|---|
| [`README.md`](README.md) | Hauptdokumentation, Feature-Übersicht, CLI-Verwendung & V1→V2 Migrationsanleitung |
| [`SECURITY.md`](SECURITY.md) | Responsible Disclosure Policy, SLAs (48h/5d/90d), Scope, Meldekanal (`security@pansi.eu`) |
| [`SECURITY_AUDIT.md`](SECURITY_AUDIT.md) | Vollständiges Log aller 39 behobenen Audit-Findings + R-NEW-1 + Tool-Audits |
| [`THREAT_MODEL.md`](THREAT_MODEL.md) | Bedrohungsmodell, Angreiferprofile und Sicherheitsannahmen |
| [`RELEASE_PROCESS.md`](RELEASE_PROCESS.md) | Multi-Platform CI/CD Release-Architektur, Toolchain-Pins und Minisign-Signierung |
| [`QUICKSTART.md`](QUICKSTART.md) | Kompakte Schritt-für-Schritt-Anleitung für Endanwender |
| [`INSTALL.md`](INSTALL.md) | Installationsanweisungen (Binaries, Scoop, WinGet, Cargo) |
| [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog Versionshistorie (aktuell: v0.9.5) |
| [`deny.toml`](deny.toml) | Strikte Konfiguration für `cargo-deny` (Advisories, Bans, Lizenzen, Quellen) |
| [`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml) | Automatisierte CI/CD Release-Pipeline für Windows & Linux musl |
---
## 4. Richtlinien für Agenten (Quality Gates)
Jede Code- oder Konfigurationsänderung **MUSS** vor einem Release oder Commit folgende Gates erfolgreich und ohne Warnungen durchlaufen:
1. **Formatierung**:
```powershell
cargo fmt --check
```
2. **Statische Code-Analyse (Linter)**:
```powershell
cargo clippy --all-targets -- -D warnings
```
3. **Vollständige Testsuite** (aktuell 142 Tests):
```powershell
cargo test --all
```
4. **Supply-Chain & Lizenz-Governance**:
```powershell
cargo-deny check
```
5. **Secret-Leak-Prävention**:
```powershell
gitleaks detect --verbose
```
*(Erlaubte Test-Mocks in `.gitleaksignore` pflegen).*
---
## 5. Coding-Konventionen
- **Keine Panics im Produktivcode**: Verwende `anyhow::Result`, `context(...)` oder `bail!` anstelle von `.unwrap()` oder `.expect()`.
- **Pfad- und Knotennamens-Sicherheit**: Alle neuen Dateinamen-Operationen müssen `validate_node_name` / `validate_path_safety` durchlaufen (Abweisung von Windows-Reservierungen wie `CON`, `PRN`, ungültigen Zeichen `<>:"/\|?*` und Steuerzeichen).
- **Speichersicherheit**: Alle sensiblen Schlüsselstrukturen müssen `Zeroize` implementieren und mit `Zeroizing<T>` gewrappt werden.
- **PowerShell-Syntax**: Befehle in PowerShell mit `;` verketten (nicht `&&`). Keine `cd`-Befehle verwenden.
+47
View File
@@ -5,6 +5,53 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/) Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/). und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
## [0.9.5] - 2026-09-22
### Transcript-Version & Upgrade-MAC (F-04, F-05)
Dieses Patch-Release schließt die verbliebenen Bindungs- und Zustands-Schwachstellen im Format-V3-Kryptosystem:
- **F-04 (MITTEL): Schließung des unbegrenzten LegacyValid-Rollback-Fensters**:
- *Problem*: In v0.9.4 blieb `LegacyValid` zeitlich unbegrenzt aktiv. Ein Angreifer mit Container-Schreibzugriff konnte nach einem legitimen 0.9.4-Mount einen alten 0.9.3-MAC-Blob und alte Chunk-Zeilen replizieren, was zu einem unbemerkten Content-Rollback führte.
- *Fix*: Erweiterung des HMAC-Transcript-Inputs um `transcript_ver` (Little-Endian u32):
`b"SANCTUM_META_V3\0" || metadata_gen (LE64) || transcript_ver (LE32) || canonical`.
- `transcript_ver = 2`: Format v0.9.4+ (Knoten + sortierte Chunk-Tupel `(node_id, chunk_index, generation)`).
- Verifikations-Kaskade:
1. Prüfung mit `ver=2` und aktuellem V2-Transcript -> `Valid`. (Verhindert jeglichen Legacy-Fallback, wenn V2 passt).
2. Schlägt V2 fehl und ist `transcript_ver >= 2` in SQLite hinterlegt -> strikt `Invalid` (schließt Replay alter 0.9.3-Blobs nach Rebind aus).
3. Schlägt V2 fehl und ist `transcript_ver < 2` (Altsystem):
- Prüfung gegen natives 0.9.4-Transcript (ohne Versionsfeld + V2-Transcript) -> `NeedsTranscriptVer`.
- Prüfung gegen natives 0.9.3-Transcript (ohne Versionsfeld oder ver=1 + Legacy-Transcript) -> `LegacyValid`.
4. Andernfalls -> strikt `Invalid`.
- Transparentes Rebind beim Mounten: Sowohl `NeedsTranscriptVer` als auch `LegacyValid` werden beim regulären Mounten transparent auf `transcript_ver = 2` migriert.
- Datenbankschema: Ergänzung der Spalte `transcript_ver INTEGER NOT NULL DEFAULT 1` in Tabelle `meta`.
- **F-05 (NIEDRIG / Verfügbarkeit): Atomare MAC-Persistierung & Zombie-Container-Reparatur**:
- *Problem*: `upgrade_to_v3` committete `version = 3` in der Datenbank *vor* dem Errechnen und Schreiben des initialen Metadaten-MACs. Ein Absturz in diesem Fenster hinterließ einen Zombie-Container (`version = 3`, `metadata_mac IS NULL`), der nicht mehr gemountet werden konnte.
- *Fix*: Berechnung und Schreiben des Metadaten-MACs (mit `transcript_ver = 2`, `gen = 1`) innerhalb der identischen atomaren SQLite-Transaktion wie die Chunk-Umschlüsselung vor `tx.commit()`.
- *Reparaturpfad*: Befindet sich ein Container im Zombie-Zustand (`version >= 3`, `metadata_mac IS NULL`, `restore_nonce IS NULL`), repariert `sanctum upgrade-format` diesen automatisch durch Errechnen und Persistieren des V2-MACs.
- *Fail-Closed Guard*: Ist `restore_nonce IS NOT NULL`, bricht die Reparatur strikt ab, um die Autorisierungsgarantien von F-02 (`--rebuild-mac` / interaktives `"JA"`) zu bewahren.
- *Mount-Führung*: Beim Versuch, einen Zombie-Container zu mounten, weist Sanctum den Benutzer gezielt auf `sanctum upgrade-format` hin.
## [0.9.4] - 2026-09-22
### Format-V3 Systembindung & Integritäts-Härtung (F-01, F-02, F-03)
Dieses Patch-Release schließt kritische Bindungs- und Replay-Schwächen im Format-V3-Kryptosystem. Es behebt den PendingRebuild-Bypass bei Header-Wiederherstellungen (F-02), integriert Chunk-Generationen in das kanonische Metadaten-MAC-Transcript zur Abwehr von Content-Replay-Angriffen gleicher Dateigröße (F-01) und stellt die vollständige Transaktions-Atomarität des Format-Upgrades auf V3 sicher (F-03).
#### Sicherheits-Fixes
- **F-02 (HOCH): Schließung des PendingRebuild-Bypasses bei manipuliertem Header**:
- *Problem / Bypass in v0.9.2 und v0.9.3*: Die Prüfung `metadata_gen == 0 && metadata_mac IS NULL -> PendingRebuild` erlaubte einem Angreifer mit Schreibzugriff auf die Containerdatei, `meta` auf `NULL` zu setzen und Decoy-Dateien umzubenennen/zu verschieben. Der nächste legitime `sanctum mount` signierte die Manipulation stillschweigend.
- *Fix*: Header-Wiederherstellungen via `restore_header_backup` erzeugen nun ein 32-Byte CSPRNG-Token `restore_nonce` in `meta`. `PendingRebuild` wird ausschließlich akzeptiert, wenn dieses Token vorhanden und `metadata_gen == 0` ist.
- *Autorisierungszwang*: `sanctum mount` erfordert nach einem Restore zwingend das neue Flag `--rebuild-mac` oder eine explizite interaktive Eingabe von `JA` mit lauter Sicherheitswarnung. `-y` allein autorisiert keinen Rebuild. `NULL`-MAC ohne Restore-Token wird strikt als `Invalid` abgewiesen (`bail!`). Nach erfolgreichem Rebuild wird `restore_nonce` gelöscht.
- **F-01 (HOCH): Chunk-Generation-Replay im Transcript & Transkript-Bindung**:
- *Problem*: K-02 band die `generation` lediglich in die AAD der gleichen Zeile ein. Ein Angreifer konnte eine Datei gleicher Größe überschreiben und anschließend eine alte Chunk-Zeile (ct, nonce, tag, generation) zurückkopieren. Da das K-01 Transcript bisher nur `chunk_count` umfasste, blieb der Metadaten-MAC gültig und die AEAD-Entschlüsselung lieferte unbemerkt veralteten Klartext. K-02 ohne Transcript-Bindung war kein vollständiger Speicher-Replay-Schutz.
- *Fix*: Das kanonische Transcript pro Vault wurde um sortierte Chunk-Tupel erweitert:
`ORDER BY node_id, chunk_index: node_id LE64 ‖ chunk_index LE32 ‖ generation LE64` (kein Ciphertext im MAC).
Jede Schreiboperation (`write_chunk_and_update_size`, Truncate, Delete) bindet die neuen Generationen in den Metadaten-MAC ein.
- *Migration bestehender V3-Container (§7)*: Ältere v0.9.2/v0.9.3-Container werden gegen das bisherige Knoten-Transcript geprüft. Ist der alte MAC valide (`LegacyValid`), wird das Transcript beim ersten Mounten transparent auf das neue Format mit Chunk-Generationen aktualisiert. Bei ungültigem MAC bricht Sanctum fail-closed ab.
- *Test-Präzisierung*: Der bestehende AAD-Test in `src/crypto.rs` wurde in `test_k02_chunk_generation_aead_binding` umbenannt, um klarzustellen, dass er nur die AEAD-Bindung im Header und nicht den Replay-Schutz gegen Zeilen-Ersetzung nachweist. Ein neuer End-to-End-Test verifiziert den Replay-Schutz gegen Zeilen-Ersetzung.
- **F-03 (HOCH): upgrade_to_v3 Transaktions-Atomarität & Dual-Slot-Konsistenz**:
- *Problem*: `upgrade_to_v3` übersprang fehlerhafte Chunks mit `if let Ok(...)` stumm, aktualisierte nur Slot 0 auf `version = 3`, ließ Slot 1 auf Version 2 zurück und meldete fälschlich Erfolg. Bei Hidden-Mounts führte die inkonsistente Slot-Version zu AAD-Fehlern.
- *Fix*: Das gesamte Format-Upgrade läuft nun in einer einzigen atomaren SQLite-Transaktion. Fehler bei `decrypt_chunk` oder `encrypt_chunk` brechen sofort via `?` ab und rollen die Transaktion vollständig zurück (`version` bleibt 2, keine halben V3-Zeilen). Chunks, die nicht mit DEK_0 entschlüsseln, brechen fail-closed ab (bei Legacy-Hidden wird das zweite Passwort verlangt).
- *Synchrone Versionierung*: Nach erfolgreichem Durchlauf wird `version = 3` synchron für Slot 0 und Slot 1 (sowie Dummy-Slots) gesetzt. Slot 0 Metadaten-MAC wird aufgebaut; Slot 1 nur, falls echte SQLite-Knoten unter Root 2 existieren. Carrier-Inner bleibt Carrier-AEAD.
## [0.9.3] - 2026-09-21 ## [0.9.3] - 2026-09-21
### Sicherheits-Governance, Supply-Chain-Audit & Dokumentationsabgleich ### Sicherheits-Governance, Supply-Chain-Audit & Dokumentationsabgleich
Generated
+1 -1
View File
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "sanctum" name = "sanctum"
version = "0.9.3" version = "0.9.5"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "sanctum" name = "sanctum"
version = "0.9.3" version = "0.9.5"
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"
+5
View File
@@ -0,0 +1,5 @@
# Antigravity & Gemini Project Context
Die vollständigen Projektrichtlinien, die Verlinkung zur Entstehungs- und Audit-Konversation, die Architektur-Dokumentation sowie die verbindlichen Quality Gates für Sanctum befinden sich in:
👉 **[AGENTS.md](AGENTS.md)**
+24 -5
View File
@@ -31,10 +31,10 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
Zufälliger 256-Bit Schlüssel via CSPRNG (`OsRng`). Der DEK wird mit dem KEK via AES-256-GCM verschlüsselt und im Header abgelegt. Zufälliger 256-Bit Schlüssel via CSPRNG (`OsRng`). Der DEK wird mit dem KEK via AES-256-GCM verschlüsselt und im Header abgelegt.
- **Speichersicherheit (Zeroize & VirtualLock)**: - **Speichersicherheit (Zeroize & VirtualLock)**:
Alle Schlüsselstrukturen implementieren das `Zeroize`-Trait (`Zeroizing<[u8; 32]>`), um sensible Schlüsseldaten beim Verlassen des Gültigkeitsbereichs im RAM sofort sicher zu nullen. Schlüsseldaten werden mittels `VirtualLock` / `mlock` vor Paging geschützt. Alle Schlüsselstrukturen implementieren das `Zeroize`-Trait (`Zeroizing<[u8; 32]>`), um sensible Schlüsseldaten beim Verlassen des Gültigkeitsbereichs im RAM sofort sicher zu nullen. Schlüsseldaten werden mittels `VirtualLock` / `mlock` vor Paging geschützt.
- **Chunk-Verschlüsselung (AES-256-GCM)**: - **Chunk-Verschlüsselung (AES-256-GCM & 24-Byte AAD, K-02)**:
Dateien werden in Blöcken von 1 MB verschlüsselt. Dateien werden in Blöcken von 1 MB verschlüsselt. Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE), `chunk_index` (8 Bytes LE) und `generation` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen oder Zurücksetzen von Chunks führt zum Authentifizierungsfehler.
- **Swap-Attack-Schutz**: - **Metadaten-Authentifizierung & Replay-Schutz (HMAC-SHA256, K-01 / F-01 / F-04)**:
Als Authenticated Associated Data (AAD) werden `node_id` (8 Bytes LE) und `chunk_index` (8 Bytes LE) an jeden Block gebunden. Ein Vertauschen von Chunks zwischen Dateien oder innerhalb einer Datei führt zum Authentifizierungsfehler. Ein kryptografischer HMAC-SHA256 (abgeleitet via HKDF `SANCTUM_META_MAC_V3`) bindet den gesamten Verzeichnisbaum deterministisch an den Datenschlüssel (DEK). Das kanonische Transcript sichert nicht nur Inode-Metadaten (Namen, Pfade, Größen, Zeitstempel), sondern auch die sortierte Sequenz aller Chunk-Generationen (`node_id LE64 ‖ chunk_index LE32 ‖ generation LE64`, F-01). Der HMAC-Input bindet zudem eine explizite Transkript-Version (`b"SANCTUM_META_V3\0" ‖ metadata_gen LE64 ‖ transcript_ver LE32 ‖ canonical`, F-04) ein, was Replay-Angriffe mit alten Metadaten-Blobs nach erfolgter Migration dauerhaft ausschließt.
- **Dual-Vault (Multi-Slot & Carrier)**: - **Dual-Vault (Multi-Slot & Carrier)**:
Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Second Safe (Hidden Vault) oder CSPRNG-Dummy. Dient dem Schutz vor neugierigen Blicken oder beiläufigem Zwang im Alltag. (Hinweis: Die Trägerdatei besitzt hohe Entropie und ist forensisch nachweisbar; kein Anspruch auf juristisch unnachweisbare Abstreitbarkeit gegen behördliche Beschlagnahme). Konstante 2-Slot-Architektur. Slot 0 dient als Standard-/Decoy-Vault, Slot 1 als Second Safe (Hidden Vault) oder CSPRNG-Dummy. Dient dem Schutz vor neugierigen Blicken oder beiläufigem Zwang im Alltag. (Hinweis: Die Trägerdatei besitzt hohe Entropie und ist forensisch nachweisbar; kein Anspruch auf juristisch unnachweisbare Abstreitbarkeit gegen behördliche Beschlagnahme).
*Carrier-Format V2 mit Paged Manifest*: Das steganografische Dateisystem des Hidden Vaults nutzt eine skalierbare Paged-Manifest-Architektur. Blöcke 0 und 1 speichern den redundanten Superblock (C-02), während Inodes über dedizierte Inode-Pages (~1 MB Nutzdaten je Seite, ca. 4.5007.000 Inodes pro Seite) dynamisch aus dem Blockpool verwaltet werden. Die Kapazität ist nicht mehr auf 7.000 Dateien limitiert, sondern skaliert dynamisch mit den verfügbaren Trägerblöcken. Robuste Fail-Soft-Resilienz (D-01) isoliert Seitenbeschädigungen, ein In-Memory Sekundärindex (D-02) beschleunigt Pfadoperationen auf O(Geschwister), und Sanctum warnt beim Einbinden automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität). *Carrier-Format V2 mit Paged Manifest*: Das steganografische Dateisystem des Hidden Vaults nutzt eine skalierbare Paged-Manifest-Architektur. Blöcke 0 und 1 speichern den redundanten Superblock (C-02), während Inodes über dedizierte Inode-Pages (~1 MB Nutzdaten je Seite, ca. 4.5007.000 Inodes pro Seite) dynamisch aus dem Blockpool verwaltet werden. Die Kapazität ist nicht mehr auf 7.000 Dateien limitiert, sondern skaliert dynamisch mit den verfügbaren Trägerblöcken. Robuste Fail-Soft-Resilienz (D-01) isoliert Seitenbeschädigungen, ein In-Memory Sekundärindex (D-02) beschleunigt Pfadoperationen auf O(Geschwister), und Sanctum warnt beim Einbinden automatisch bei Blockknappheit (< 20 freie Blöcke oder < 5% Restkapazität).
@@ -160,10 +160,29 @@ sanctum.exe backup --path "C:\Pfad\tresor.sanctum" --output "D:\Backup\tresor_ba
# Container aus Backup wiederherstellen: # Container aus Backup wiederherstellen:
sanctum.exe restore --path "D:\Backup\tresor_backup.sanctum" --output "C:\Pfad\tresor_restored.sanctum" sanctum.exe restore --path "D:\Backup\tresor_backup.sanctum" --output "C:\Pfad\tresor_restored.sanctum"
# Vollständige Integritätsprüfung (B-Tree, Knoten und AEAD-Tags aller Chunks): # Header in separate Sicherungsdatei (.sanctum.hdr) sichern:
sanctum.exe backup-header --path "C:\Pfad\tresor.sanctum" --output "D:\Backup\tresor.hdr"
# Header aus Sicherungsdatei (.sanctum.hdr) wiederherstellen:
sanctum.exe restore-header --path "C:\Pfad\tresor.sanctum" --header "D:\Backup\tresor.hdr"
# Nach Header-Wiederherstellung: Metadaten-MAC mit autorisierter Neubindung mounten (F-02):
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --rebuild-mac
# Vollständige Integritätsprüfung (B-Tree, Knoten, Chunk-AEAD und Metadaten-MAC):
sanctum.exe verify --path "C:\Pfad\tresor.sanctum" sanctum.exe verify --path "C:\Pfad\tresor.sanctum"
``` ```
> [!IMPORTANT]
> **Sicherheitsgarantie bei Header-Wiederherstellungen (F-02)**: Nach einem `restore-header` setzt Sanctum ein einmaliges 32-Byte CSPRNG-Token (`restore_nonce`). Der nachfolgende `mount` verlangt zwingend `--rebuild-mac` (oder eine interaktive Bestätigung mit `JA`), um den Metadaten-MAC neu aufzubauen und das Token zu löschen. Ein stillschweigendes Signieren manipulierter Metadaten ist ausgeschlossen; `-y` allein autorisiert keinen Rebuild.
```powershell
# Ältere V2-Container auf Format V3 aktualisieren (atomar & transaktional, F-03):
sanctum.exe upgrade-format --path "C:\Pfad\tresor.sanctum"
```
> [!NOTE]
> `upgrade-format` führt die Umschlüsselung aller Chunks von 16-Byte- auf 24-Byte-AAD und die Aktualisierung beider Header-Slots auf `version = 3` in einer **einzigen atomaren SQLite-Transaktion** durch. Bei Fehlern greift ein automatischer Rollback ohne inkonsistente Mischzustände.
--- ---
### 6. Storage Compaction (Speicherbereinigung) ### 6. Storage Compaction (Speicherbereinigung)
+1 -1
View File
@@ -10,7 +10,7 @@ Sicherheitsupdates und Patches werden jeweils für die neueste Version von Sanct
| Version | Status | Sicherheits-Support | | Version | Status | Sicherheits-Support |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| **v0.9.x** | **Aktiv (Aktuell: v0.9.2)** | **Vollständig unterstützt** | | **v0.9.x** | **Aktiv (Aktuell: v0.9.5)** | **Vollständig unterstützt** |
| <= v0.8.x | Veraltet | Nicht mehr unterstützt (Upgrade empfohlen) | | <= v0.8.x | Veraltet | Nicht mehr unterstützt (Upgrade empfohlen) |
--- ---
+27 -3
View File
@@ -1,12 +1,13 @@
# Sanctum Security Audit & Remediation Log (v0.9.3) # Sanctum Security Audit & Remediation Log (v0.9.5)
Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheitsaudits sowie nachfolgende Härtungen zusammen, dokumentiert die angewandten Härtungsmaßnahmen, referenziert die jeweiligen Git-Commits und benennt die zugehörigen automatisierten Regressionstests. Abschnitt 5 dokumentiert die Formaterweiterung Carrier V2 (Paged Manifest) aus v0.9.0, Abschnitt 6 die Minor Issues aus v0.9.1, Abschnitt 7 den Recovery-MAC-Fix (R-NEW-1) aus v0.9.2 und Abschnitt 8 die Supply-Chain- und Secret-Audits aus v0.9.3. Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheitsaudits sowie nachfolgende Härtungen zusammen, dokumentiert die angewandten Härtungsmaßnahmen, referenziert die jeweiligen Git-Commits und benennt die zugehörigen automatisierten Regressionstests. Abschnitt 5 dokumentiert die Formaterweiterung Carrier V2 (Paged Manifest) aus v0.9.0, Abschnitt 6 die Minor Issues aus v0.9.1, Abschnitt 7 den Recovery-MAC-Fix (R-NEW-1) aus v0.9.2, Abschnitt 8 die Supply-Chain- und Secret-Audits aus v0.9.3, Abschnitt 9 die Format-V3 Systembindungshärtungen (F-01, F-02, F-03) aus v0.9.4 und Abschnitt 10 die Transcript-Versionierung und atomare Upgrade-Persistierung (F-04, F-05) aus v0.9.5.
--- ---
## Audit-Zusammenfassung ## Audit-Zusammenfassung
- **Zielversion:** Sanctum v0.9.3 (Basis v0.9.2) - **Status:** Vollständig gehärtet & verifiziert
- **Zielversion:** Sanctum v0.9.5 (Basis v0.9.4)
- **Behobene Findings:** 39 / 39 (100%) Audit-Findings + R-NEW-1 - **Behobene Findings:** 39 / 39 (100%) Audit-Findings + R-NEW-1
- **Quality Gates:** `cargo fmt --check` (100% sauber), `cargo clippy --all-targets -- -D warnings` (0 Warnungen), `cargo-deny` (100% ok), `gitleaks` (0 Leaks) - **Quality Gates:** `cargo fmt --check` (100% sauber), `cargo clippy --all-targets -- -D warnings` (0 Warnungen), `cargo-deny` (100% ok), `gitleaks` (0 Leaks)
- **Container-Format:** Container-Format V3 mit kanonischer Metadaten-Authentifizierung (HMAC-SHA256) beim Mount und Chunk-Replay-Schutz (Generation-gebundenes AAD). - **Container-Format:** Container-Format V3 mit kanonischer Metadaten-Authentifizierung (HMAC-SHA256) beim Mount und Chunk-Replay-Schutz (Generation-gebundenes AAD).
@@ -163,6 +164,29 @@ Dieses Dokument fasst alle 39 Findings aus drei umfassenden externen Sicherheits
- **Responsible Disclosure Policy (`SECURITY.md`):** - **Responsible Disclosure Policy (`SECURITY.md`):**
- Einführung von `SECURITY.md` mit SLA (48h Reaktionszeit, 5 Tage Bewertung), Kontaktadresse (`security@pansi.eu`), Gitea Security Advisories Kanal und 90-Tage Embargo. - Einführung von `SECURITY.md` mit SLA (48h Reaktionszeit, 5 Tage Bewertung), Kontaktadresse (`security@pansi.eu`), Gitea Security Advisories Kanal und 90-Tage Embargo.
### 9. Sanctum v0.9.4 — Format-V3 Systembindung (F-01, F-02, F-03)
- **PendingRebuild-Bypass durch Restore-Token geschlossen (F-02, HIGH):**
- Verhindert, dass Angreifer durch Setzen von `metadata_mac = NULL` und `metadata_gen = 0` manipulierte Metadatenbäume stillschweigend beim Mount signieren lassen.
- Ein 32-Byte CSPRNG-Token (`restore_nonce`) wird ausschließlich bei `restore_header_backup` in `meta` hinterlegt.
- `mount` fordert zwingend `--rebuild-mac` oder eine interaktive Bestätigung (`"JA"` via `is_terminal()`). Ohne Token wird `NULL`-MAC als ungültig zurückgewiesen (`MetadataMacStatus::Invalid`). Nach Rebuild wird das Token gelöscht.
- **Chunk-Generation Replay-Attacke behoben (F-01, HIGH):**
- Der kanonische HMAC-SHA256 Transkript (`SANCTUM_META_MAC_V3`) bindet nun neben sortierten Knoten (`ORDER BY id ASC`) auch alle sortierten Chunk-Tupel (`ORDER BY node_id, chunk_index: node_id LE64 || chunk_index LE32 || generation LE64`).
- Ein Angreifer kann keine alten Chunk-Ciphertexte mit alter Generation wiederherstellen, selbst bei identischer Dateigröße.
- Transparente Migration für bestehende Container, deren alter Knoten-Transkript verifiziert (`MetadataMacStatus::LegacyValid`).
- **Atomares V2-to-V3 Upgrade (F-03, HIGH):**
- `upgrade_to_v3` läuft in einer atomaren SQLite-Transaktion mit Fail-Closed-Verhalten und Rollback bei Chunk-Fehlern.
- Sowohl Slot 0 als auch Slot 1 werden konsistent auf `version = 3` aktualisiert, um Korruption in Dual-Vault-Szenarien auszuschließen.
- Neuer Metadaten-MAC wird direkt im Upgrade-Lauf persistiert.
### 10. Sanctum v0.9.5 — Transcript-Version & Upgrade-MAC (F-04, F-05)
- **Schließung des unbegrenzten LegacyValid-Rollback-Fensters (F-04, MEDIUM):**
- Erweiterung des kanonischen HMAC-SHA256 Inputs um ein 4-Byte Transkript-Versionsfeld: `b"SANCTUM_META_V3\0" ‖ metadata_gen (LE64) ‖ transcript_ver (LE32) ‖ canonical`.
- Strikte Verifikations-Kaskade: Container mit persistiertem `transcript_ver >= 2` lehnen alte 0.9.3-Transkripte kompromisslos als `Invalid` ab, wodurch ein nachträglicher Content-Rollback via Altdaten-Replay unmöglich wird.
- Transparentes Rebind: Ältere Container (`NeedsTranscriptVer` aus v0.9.4 und `LegacyValid` aus v0.9.3) werden beim Mounten automatisch auf `transcript_ver = 2` angehoben.
- **Atomare MAC-Persistierung & Zombie-Container-Reparatur (F-05, LOW / Verfügbarkeit):**
- `upgrade_to_v3` persistiert den Metadaten-MAC (`transcript_ver = 2`, `gen = 1`) in derselben Transaktion vor dem Commit, wodurch Absturzfenster mit `version = 3` und `metadata_mac IS NULL` eliminiert sind.
- Automatischer Reparaturpfad in `sanctum upgrade-format` für unvollständig migrierte Altsysteme, mit striktem Abbruch, falls `restore_nonce IS NOT NULL` gesetzt ist (Erhalt der F-02 Schutzgarantien).
--- ---
## Verifikationsnachweis ## Verifikationsnachweis
+21 -2
View File
@@ -1,6 +1,6 @@
# Threat Model & Sicherheitsarchitektur von Sanctum # Threat Model & Sicherheitsarchitektur von Sanctum
Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.9.3** im reinen Userland-Betrieb. Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die Schutzmechanismen von **Sanctum v0.9.5** im reinen Userland-Betrieb.
--- ---
@@ -23,7 +23,7 @@ Dieses Dokument beschreibt das Bedrohungsmodell, die Sicherheitsannahmen und die
| Angreifer / Bedrohung | Beschreibung | Sanctum-Gegenmaßnahme | | Angreifer / Bedrohung | Beschreibung | Sanctum-Gegenmaßnahme |
|---|---|---| |---|---|---|
| **Kalter Datenträger-Angriff** | Angreifer hat physischen oder dateibasierten Zugriff auf die `.sanctum`-Datei auf USB-Stick, SSD oder Cloud-Storage. | Argon2id KDF (256 MiB, t=4, p=4), AES-256-GCM mit eindeutiger Chunk-Generation AAD, kanonische HMAC-SHA256 Metadaten-Authentifizierung (Format V3). | | **Kalter Datenträger-Angriff** | Angreifer hat physischen oder dateibasierten Zugriff auf die `.sanctum`-Datei auf USB-Stick, SSD oder Cloud-Storage. | Argon2id KDF (256 MiB, t=4, p=4), AES-256-GCM mit eindeutiger Chunk-Generation AAD, kanonische HMAC-SHA256 Metadaten-Authentifizierung mit Transkript-Versionierung (Format V3.2, F-01/F-04). |
| **Böswillige Manipulation / Bitrot** | Gezieltes Verändern von Metadaten oder Chunks im Speicher. | AEAD-Tags auf allen Datenblöcken; `sanctum verify` erkennt jede Modifikation; Schreib- und Leseoperationen verwerfen manipulierte Blöcke sofort (`Fail-Closed`). | | **Böswillige Manipulation / Bitrot** | Gezieltes Verändern von Metadaten oder Chunks im Speicher. | AEAD-Tags auf allen Datenblöcken; `sanctum verify` erkennt jede Modifikation; Schreib- und Leseoperationen verwerfen manipulierte Blöcke sofort (`Fail-Closed`). |
| **Replay- & Chunk-Vertauschungsangriffe** | Vertauschen von Chunks zwischen Dateien oder Einspielen alter Versionen. | Kryptografische Bindung aller Chunks an `(node_id, chunk_index, generation)` in den AEAD Additional Authenticated Data (AAD). | | **Replay- & Chunk-Vertauschungsangriffe** | Vertauschen von Chunks zwischen Dateien oder Einspielen alter Versionen. | Kryptografische Bindung aller Chunks an `(node_id, chunk_index, generation)` in den AEAD Additional Authenticated Data (AAD). |
| **Lokale Benutzerisolation** | Mehrbenutzersysteme: Andere Standardbenutzer auf demselben Rechner. | Windows- und Unix-Dateirechte; TCP-Bind ausschließlich an Loopback `127.0.0.1`; dynamisches 128-Bit Session-Token. | | **Lokale Benutzerisolation** | Mehrbenutzersysteme: Andere Standardbenutzer auf demselben Rechner. | Windows- und Unix-Dateirechte; TCP-Bind ausschließlich an Loopback `127.0.0.1`; dynamisches 128-Bit Session-Token. |
@@ -140,4 +140,23 @@ Frühere Versionen von Sanctum (vor v0.8.0) verwendeten leere AAD für Dateiname
- **Risiko**: Bei aktiviertem `--legacy-names` ist der Schutz vor Vertauschen von Dateinamen aufgehoben. Ein Angreifer könnte Dateien manipulieren, indem er verschlüsselte Namen zwischen verschiedenen Verzeichnissen vertauscht. - **Risiko**: Bei aktiviertem `--legacy-names` ist der Schutz vor Vertauschen von Dateinamen aufgehoben. Ein Angreifer könnte Dateien manipulieren, indem er verschlüsselte Namen zwischen verschiedenen Verzeichnissen vertauscht.
- **Empfehlung**: Dieses Flag darf **ausschließlich** zur einmaligen Migration von Altdaten verwendet werden. Im regulären Betrieb muss es deaktiviert bleiben. - **Empfehlung**: Dieses Flag darf **ausschließlich** zur einmaligen Migration von Altdaten verwendet werden. Im regulären Betrieb muss es deaktiviert bleiben.
---
## 8. Format-V3 Systembindung & Integritätshärtung (F-01 bis F-05)
### 8.1 Transkript-Bindung & Replay-Schutz (F-01, F-04)
- **Problem**: Bei isolierter AEAD-Bindung der Generation im Chunk-Header (`node_id ‖ chunk_index ‖ generation`) konnte ein Angreifer alte Chunk-Zeilen (`ct, nonce, tag, generation`) wiederherstellen, wenn die Datei dieselbe Größe behielt.
- **Kanonischer HMAC**: Sanctum bindet alle sortierten Chunk-Tupel (`ORDER BY node_id, chunk_index: node_id LE64 ‖ chunk_index LE32 ‖ generation LE64`) direkt in das Metadaten-Transkript ein.
- **Transkript-Versionierung (F-04)**: Um ein unbegrenztes Rollback auf v0.9.3-Metadaten-Blobs (`LegacyValid`) zu verhindern, umfasst der HMAC-Transkript-Input die Little-Endian `transcript_ver`:
`b"SANCTUM_META_V3\0" ‖ metadata_gen (LE64) ‖ transcript_ver (LE32) ‖ canonical`.
Sobald ein Container auf `transcript_ver = 2` migriert wurde, lehnt Sanctum ältere 0.9.3-Transkripte strikt als `Invalid` ab.
### 8.2 Autorisierter Header-Rebuild (F-02)
- Ein Rebuild des Metadaten-MACs (`metadata_gen == 0 && metadata_mac IS NULL`) nach einem Header-Restore wird ausschließlich zugelassen, wenn ein kryptografisches CSPRNG-Token (`restore_nonce`) in `meta` existiert und der Benutzer explizit `--rebuild-mac` übergibt oder interaktiv `"JA"` bestätigt. Manipulierte SQLite-Header ohne dieses Token werden unweigerlich abgewiesen.
### 8.3 Transaktions-Atomarität & Resilienz (F-03, F-05)
- Das Format-Upgrade (`upgrade_to_v3`) wird in einer einzigen atomaren SQLite-Transaktion ausgeführt. Bei jeglichem Entschlüsselungs- oder Umschlüsselungsfehler erfolgt ein vollständiger Rollback auf Version 2.
- Der initiale Metadaten-MAC (`transcript_ver = 2`, `gen = 1`) wird vor dem Transaktions-Commit berechnet und geschrieben (F-05), sodass keine ungeschützten Zombie-Container (`version = 3, metadata_mac IS NULL`) entstehen können.
- Im Falle eines abrupten Stromausfalls vor Commit verbleibt der Container sauber auf Version 2. Sollte ein Altsystem dennoch in einen Zombie-Zustand geraten sein, bietet `sanctum upgrade-format` eine automatische Reparatur, sofern kein unautorisierter Restore vorliegt (`restore_nonce IS NULL`).
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"version": "0.9.3", "version": "0.9.5",
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV", "description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
"homepage": "https://gitea.pansi.eu/harald/sanctum", "homepage": "https://gitea.pansi.eu/harald/sanctum",
"license": "MIT", "license": "MIT",
"architecture": { "architecture": {
"64bit": { "64bit": {
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.3/sanctum-v0.9.3-windows-x86_64.zip", "url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.5/sanctum-v0.9.5-windows-x86_64.zip",
"hash": "75611155008ed0ecd6e6bbbb77fc549c5884ecc5779ca3012e66852bbb3b729b", "hash": "f47d43e455a73d6ed0affa7af07437b82c7f61188aade9b2a46cbb4ea43968fc",
"bin": "sanctum.exe" "bin": "sanctum.exe"
} }
}, },
+4 -4
View File
@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json # yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
PackageIdentifier: HaraldPansi.Sanctum PackageIdentifier: HaraldPansi.Sanctum
PackageVersion: 0.9.3 PackageVersion: 0.9.5
PackageName: Sanctum PackageName: Sanctum
Publisher: Harald Pansi Publisher: Harald Pansi
PublisherUrl: https://gitea.pansi.eu/harald PublisherUrl: https://gitea.pansi.eu/harald
@@ -18,7 +18,7 @@ Tags:
- webdav - webdav
- container - container
- plausible-deniability - plausible-deniability
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.3 ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.5
Installers: Installers:
- Architecture: x64 - Architecture: x64
InstallerType: zip InstallerType: zip
@@ -26,7 +26,7 @@ Installers:
NestedInstallerFiles: NestedInstallerFiles:
- RelativeFilePath: sanctum.exe - RelativeFilePath: sanctum.exe
PortableCommandAlias: sanctum PortableCommandAlias: sanctum
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.3/sanctum-v0.9.3-windows-x86_64.zip InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.5/sanctum-v0.9.5-windows-x86_64.zip
InstallerSha256: 75611155008ed0ecd6e6bbbb77fc549c5884ecc5779ca3012e66852bbb3b729b InstallerSha256: f47d43e455a73d6ed0affa7af07437b82c7f61188aade9b2a46cbb4ea43968fc
ManifestType: singleton ManifestType: singleton
ManifestVersion: 1.6.0 ManifestVersion: 1.6.0
+8 -2
View File
@@ -81,12 +81,18 @@ $Release = $ExistingReleases | Where-Object { $_.tag_name -eq $TagName }
if (-not $Release) { if (-not $Release) {
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false) $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$Changelog = [System.IO.File]::ReadAllText((Join-Path $ProjectRoot "CHANGELOG.md"), $Utf8NoBom) $FullChangelog = [System.IO.File]::ReadAllText((Join-Path $ProjectRoot "CHANGELOG.md"), $Utf8NoBom)
$VersionPattern = "(?s)## \[$([regex]::Escape($Version))\].*?(?=(?:\r?\n## \[|\z))"
if ($FullChangelog -match $VersionPattern) {
$ReleaseBody = $matches[0].Trim()
} else {
$ReleaseBody = $FullChangelog
}
$PayloadPath = Join-Path $DistDir "release_req.json" $PayloadPath = Join-Path $DistDir "release_req.json"
$Payload = @{ $Payload = @{
tag_name = $TagName tag_name = $TagName
name = "Sanctum $TagName (Windows & Linux x86_64)" name = "Sanctum $TagName (Windows & Linux x86_64)"
body = $Changelog body = $ReleaseBody
draft = $false draft = $false
prerelease = $false prerelease = $false
} | ConvertTo-Json -Depth 5 } | ConvertTo-Json -Depth 5
+32 -1
View File
@@ -645,6 +645,34 @@ pub fn verify_metadata_mac(
computed.ct_eq(expected_mac).into() computed.ct_eq(expected_mac).into()
} }
/// Berechnet den Metadaten-MAC mit Transcript-Versionierung (Format V3.2 / F-04).
/// Format: b"SANCTUM_META_V3\0" || metadata_gen LE64 || transcript_ver LE32 || canonical
pub fn compute_metadata_mac_v2(
mac_key: &[u8; 32],
metadata_gen: u64,
transcript_ver: u32,
canonical: &[u8],
) -> [u8; 32] {
let mut data = Vec::with_capacity(28 + canonical.len());
data.extend_from_slice(b"SANCTUM_META_V3\0");
data.extend_from_slice(&metadata_gen.to_le_bytes());
data.extend_from_slice(&transcript_ver.to_le_bytes());
data.extend_from_slice(canonical);
hmac_sha256(mac_key, &data)
}
/// Prüft in konstanter Zeit, ob der gegebene Metadaten-MAC mit Transcript-Versionierung gültig ist (Format V3.2 / F-04).
pub fn verify_metadata_mac_v2(
mac_key: &[u8; 32],
metadata_gen: u64,
transcript_ver: u32,
canonical: &[u8],
expected_mac: &[u8; 32],
) -> bool {
let computed = compute_metadata_mac_v2(mac_key, metadata_gen, transcript_ver, canonical);
computed.ct_eq(expected_mac).into()
}
/// Erzeugt die 16-Byte Associated Data (AAD) für einen Chunk, um Swap-Angriffe zu verhindern: /// Erzeugt die 16-Byte Associated Data (AAD) für einen Chunk, um Swap-Angriffe zu verhindern:
/// node_id (8 Bytes Little-Endian) || chunk_index (8 Bytes Little-Endian). /// node_id (8 Bytes Little-Endian) || chunk_index (8 Bytes Little-Endian).
#[inline] #[inline]
@@ -920,8 +948,11 @@ mod tests {
.is_err()); .is_err());
} }
// F-01: Prüft die kryptografische AEAD-Bindung der Generation im AAD (Fehler bei Generationsabweichung).
// Hinweis: Dies beweist die AEAD-Bindung an den AAD-Header, NICHT jedoch den Schutz gegen Zeilen-Ersetzung
// im persistenten Speicher (dieser wird durch die Aufnahme der Generationen in das Metadaten-MAC-Transcript in F-01 garantiert).
#[test] #[test]
fn test_k02_chunk_replay_protection_with_generation_aad() { fn test_k02_chunk_generation_aead_binding() {
let dek = generate_dek(); let dek = generate_dek();
let plaintext_v1 = b"Original Chunk Data at Generation 1"; let plaintext_v1 = b"Original Chunk Data at Generation 1";
let plaintext_v2 = b"Overwritten Chunk Data at Generation 2"; let plaintext_v2 = b"Overwritten Chunk Data at Generation 2";
+27 -4
View File
@@ -117,6 +117,10 @@ enum Commands {
/// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben) /// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben)
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
stealth: bool, stealth: bool,
/// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02)
#[arg(long, default_value_t = false)]
rebuild_mac: bool,
}, },
/// Trennt ein eingebundenes Netzlaufwerk manuell /// Trennt ein eingebundenes Netzlaufwerk manuell
@@ -1350,7 +1354,20 @@ fn handle_upgrade_format(container_path: &Path) -> Result<()> {
} }
}; };
if keys.version() >= FORMAT_VERSION_V3 { // F-05: Wenn bereits V3, aber MAC fehlt (Zombie-Zustand), repariere mit upgrade_to_v3
let is_zombie = {
use rusqlite::OptionalExtension;
let conn = db.conn();
conn.query_row(
"SELECT 1 FROM meta WHERE slot_id = 0 AND version >= 3 AND metadata_mac IS NULL AND restore_nonce IS NULL",
[],
|_| Ok(true),
)
.optional()?
.unwrap_or(false)
};
if keys.version() >= FORMAT_VERSION_V3 && !is_zombie {
println!( println!(
" • Container ist bereits Format V{} (keine Migration erforderlich).", " • Container ist bereits Format V{} (keine Migration erforderlich).",
keys.version() keys.version()
@@ -1361,9 +1378,13 @@ fn handle_upgrade_format(container_path: &Path) -> Result<()> {
db.upgrade_to_v3(keys.dek())?; db.upgrade_to_v3(keys.dek())?;
db.checkpoint()?; db.checkpoint()?;
println!(" ✔ Container erfolgreich auf Format V3 aktualisiert!"); if is_zombie {
println!(" Metadaten-Authentifizierung (HMAC-SHA256) aktiviert."); println!(" Metadaten-MAC erfolgreich rekonstruiert und gebunden (Format V3 Reparatur)!");
println!(" • Replay-Schutz für Chunks (24-Byte AAD mit Generation) aktiviert."); } else {
println!(" ✔ Container erfolgreich auf Format V3 aktualisiert!");
println!(" • Metadaten-Authentifizierung (HMAC-SHA256) aktiviert.");
println!(" • Replay-Schutz für Chunks (24-Byte AAD mit Generation) aktiviert.");
}
Ok(()) Ok(())
} }
@@ -1626,6 +1647,7 @@ async fn run() -> Result<()> {
no_anti_leak, no_anti_leak,
anti_leak_list, anti_leak_list,
stealth, stealth,
rebuild_mac,
} => { } => {
let drive_char = match drive { let drive_char = match drive {
Some(ref d) => parse_drive_letter(d)?, Some(ref d) => parse_drive_letter(d)?,
@@ -1679,6 +1701,7 @@ async fn run() -> Result<()> {
anti_leak, anti_leak,
anti_leak_list.as_deref(), anti_leak_list.as_deref(),
stealth, stealth,
rebuild_mac,
) )
.await?; .await?;
} }
+68 -4
View File
@@ -1,4 +1,5 @@
use std::convert::Infallible; use std::convert::Infallible;
use std::io::IsTerminal;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
#[cfg(unix)] #[cfg(unix)]
@@ -86,6 +87,7 @@ pub async fn mount_container(
anti_leak: bool, anti_leak: bool,
anti_leak_list: Option<&Path>, anti_leak_list: Option<&Path>,
stealth: bool, stealth: bool,
rebuild_mac: bool,
) -> Result<()> { ) -> Result<()> {
let drive_str = format_drive(drive_letter); let drive_str = format_drive(drive_letter);
let _ = &drive_str; let _ = &drive_str;
@@ -187,7 +189,7 @@ pub async fn mount_container(
// S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren // S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren
let _advisory_lock = db.acquire_advisory_lock_guard(false)?; let _advisory_lock = db.acquire_advisory_lock_guard(false)?;
// K-01 / R-NEW-1: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3) // K-01 / F-01 / F-02: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3)
if version >= crate::crypto::FORMAT_VERSION_V3 { if version >= crate::crypto::FORMAT_VERSION_V3 {
match db.verify_metadata_mac_status_for_slot(vault_id, &dek) { match db.verify_metadata_mac_status_for_slot(vault_id, &dek) {
Ok(crate::storage::MetadataMacStatus::Valid) => { Ok(crate::storage::MetadataMacStatus::Valid) => {
@@ -197,19 +199,81 @@ pub async fn mount_container(
); );
} }
} }
Ok(crate::storage::MetadataMacStatus::PendingRebuild) => { Ok(crate::storage::MetadataMacStatus::NeedsTranscriptVer) => {
if !stealth { if !stealth {
println!( println!(
" • Metadaten-MAC: Metadaten-MAC nach Wiederherstellung neu aufgebaut" " • Metadaten-MAC: Format-V3 Metadaten-MAC auf Transcript-Version 2 aktualisiert (F-04)"
); );
} }
db.set_active_slot_and_dek(vault_id, dek.clone()); db.set_active_slot_and_dek(vault_id, dek.clone());
db.update_metadata_mac()?; db.update_metadata_mac()?;
db.checkpoint()?; db.checkpoint()?;
} }
Ok(crate::storage::MetadataMacStatus::LegacyValid) => {
if !stealth {
println!(
" • Metadaten-MAC: Format-V3 Metadaten-MAC von Legacy-Knoten auf Transcript-Version 2 migriert (F-01/F-04)"
);
}
db.set_active_slot_and_dek(vault_id, dek.clone());
db.update_metadata_mac()?;
db.checkpoint()?;
}
Ok(crate::storage::MetadataMacStatus::PendingRebuild) => {
let authorized = if rebuild_mac {
true
} else if std::io::stdin().is_terminal() {
eprintln!();
eprintln!(
" [!] WARNUNG: METADATEN WERDEN NEU GEBUNDEN; MAC-HISTORIE ERLISCHT."
);
eprintln!(" Der Container-Header wurde wiederhergestellt. Die Metadaten-Integrität muss");
eprintln!(" neu an den Tresor gebunden werden.");
eprintln!(" Starten Sie mit '--rebuild-mac' oder bestätigen Sie die Operation interaktiv mit 'JA'.");
eprint!(" Bestätigung ('JA' eingeben): ");
let _ = std::io::Write::flush(&mut std::io::stderr());
let mut input = String::new();
std::io::stdin().read_line(&mut input).is_ok() && input.trim() == "JA"
} else {
false
};
if !authorized {
bail!(
"Metadaten-MAC-Rebuild nicht autorisiert. Starten Sie mit '--rebuild-mac' oder tippen Sie 'JA'."
);
}
if !stealth {
println!(
" • Metadaten-MAC: Metadaten neu gebunden & MAC erfolgreich aufgebaut (F-02)"
);
}
db.set_active_slot_and_dek(vault_id, dek.clone());
db.update_metadata_mac()?;
db.clear_restore_nonce(vault_id)?;
db.checkpoint()?;
}
Ok(crate::storage::MetadataMacStatus::Invalid) => { Ok(crate::storage::MetadataMacStatus::Invalid) => {
// F-05: Prüfen, ob es sich um den unvollständigen Zombie-Zustand handelt
let is_zombie = {
use rusqlite::OptionalExtension;
let conn = db.conn();
conn.query_row(
"SELECT 1 FROM meta WHERE slot_id = ?1 AND version >= 3 AND metadata_mac IS NULL AND restore_nonce IS NULL",
[vault_id],
|_| Ok(true),
)
.optional()?
.unwrap_or(false)
};
if is_zombie {
bail!(
"Container befindet sich im unvollständigen Zustand (Format V3 ohne Metadaten-MAC). Führen Sie zur Reparatur 'sanctum upgrade-format --path <CONTAINER>' nach Entsperrung aus."
);
}
bail!( bail!(
"Metadaten-MAC-Verifikation fehlgeschlagen: Die Container-Metadaten wurden manipuliert oder sind beschädigt (K-01)." "Metadaten-MAC-Verifikation fehlgeschlagen: Die Container-Metadaten wurden manipuliert oder sind beschädigt (K-01 / F-01 / F-02 / F-04)."
); );
} }
Err(e) => { Err(e) => {
+6 -1
View File
@@ -3,6 +3,8 @@ use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use rand::rngs::OsRng;
use rand::RngCore;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::crypto::{ use crate::crypto::{
@@ -276,7 +278,10 @@ pub fn restore_header_backup(container_path: &Path, backup_path: &Path) -> Resul
let db = Database::open(container_path).context("Konnte Ziel-Containerdatei nicht öffnen")?; let db = Database::open(container_path).context("Konnte Ziel-Containerdatei nicht öffnen")?;
db.restore_meta(&meta) let mut restore_nonce = [0u8; 32];
OsRng.fill_bytes(&mut restore_nonce);
db.restore_meta_with_nonce(&meta, Some(&restore_nonce))
.context("Fehler beim Wiederherstellen der Header-Tabelle in der Datenbank")?; .context("Fehler beim Wiederherstellen der Header-Tabelle in der Datenbank")?;
db.checkpoint() db.checkpoint()
+368 -67
View File
@@ -9,10 +9,10 @@ use rusqlite::{params, Connection, OptionalExtension};
use tracing::{error, warn}; use tracing::{error, warn};
use crate::crypto::{ use crate::crypto::{
compute_metadata_mac, decrypt_node_name, derive_kek, derive_metadata_mac_key, compute_metadata_mac_v2, decrypt_node_name, derive_kek, derive_metadata_mac_key,
encrypt_node_name, generate_dummy_slot, unwrap_key_payload, validate_kdf_params, encrypt_node_name, generate_dummy_slot, unwrap_key_payload, validate_kdf_params,
verify_metadata_mac, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1, verify_metadata_mac, verify_metadata_mac_v2, KdfParams, CHUNK_SIZE, FORMAT_VERSION,
FORMAT_VERSION_V2, FORMAT_VERSION_V3, MAGIC_BYTES, FORMAT_VERSION_V1, FORMAT_VERSION_V2, FORMAT_VERSION_V3, MAGIC_BYTES,
}; };
use zeroize::Zeroizing; use zeroize::Zeroizing;
@@ -208,15 +208,21 @@ fn current_timestamp() -> u64 {
.unwrap_or(0) .unwrap_or(0)
} }
/// Status der Metadaten-MAC-Integritätsprüfung (K-01 / R-NEW-1). /// Status der Metadaten-MAC-Integritätsprüfung (K-01 / F-01 / F-02).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataMacStatus { pub enum MetadataMacStatus {
/// Metadaten-MAC ist vorhanden und stimmt mit den kanonischen Metadaten überein. /// Metadaten-MAC ist vorhanden und stimmt mit Transcript-Version 2 (inklusive Chunk-Generationen) überein (F-01 / F-04).
Valid, Valid,
/// Container-Header wurde frisch aus einem Backup restauriert (metadata_gen == 0 && metadata_mac IS NULL). /// Metadaten-MAC stammt aus v0.9.4 (Knoten + Chunk-Generationen, jedoch ohne transcript_ver im HMAC-Input).
/// Der MAC muss beim Mounten transparent mit dem aktiven DEK neu aufgebaut werden. /// Wird beim Mounten transparent auf Transcript-Version 2 aktualisiert (F-04 Migration).
NeedsTranscriptVer,
/// Metadaten-MAC stimmt mit dem älteren V3.0-Transcript (nur Knoten-Metadaten aus v0.9.3) überein.
/// Wird beim Mounten transparent auf das neue V3.2-Transcript gebunden (F-01 / F-04 Migration).
LegacyValid,
/// Container-Header wurde aus einem Backup restauriert (restore_nonce vorhanden und metadata_gen == 0 && metadata_mac IS NULL).
/// Der MAC muss beim Mounten mit --rebuild-mac (oder interaktivem 'JA') neu aufgebaut werden (F-02).
PendingRebuild, PendingRebuild,
/// Metadaten-MAC fehlt (bei gen > 0) oder stimmt nicht mit den berechneten Daten überein (Manipulationsverdacht). /// Metadaten-MAC fehlt (ohne gültigen restore_nonce) oder stimmt nicht mit den berechneten Daten überein (Manipulationsverdacht).
Invalid, Invalid,
} }
@@ -480,6 +486,15 @@ impl Database {
let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_host TEXT", []); let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_host TEXT", []);
let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_time INTEGER", []); let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_time INTEGER", []);
// Spalte restore_nonce in meta (Format V3 / F-02)
let _ = conn.execute("ALTER TABLE meta ADD COLUMN restore_nonce BLOB", []);
// Spalte transcript_ver in meta (Format V3.2 / F-04)
let _ = conn.execute(
"ALTER TABLE meta ADD COLUMN transcript_ver INTEGER NOT NULL DEFAULT 1",
[],
);
Ok(()) Ok(())
} }
@@ -646,6 +661,8 @@ impl Database {
header_tag BLOB NOT NULL, header_tag BLOB NOT NULL,
metadata_mac BLOB, metadata_mac BLOB,
metadata_gen INTEGER NOT NULL DEFAULT 0, metadata_gen INTEGER NOT NULL DEFAULT 0,
restore_nonce BLOB,
transcript_ver INTEGER NOT NULL DEFAULT 1,
lock_pid INTEGER, lock_pid INTEGER,
lock_host TEXT, lock_host TEXT,
lock_time INTEGER lock_time INTEGER
@@ -959,6 +976,8 @@ impl Database {
header_tag BLOB NOT NULL, header_tag BLOB NOT NULL,
metadata_mac BLOB, metadata_mac BLOB,
metadata_gen INTEGER NOT NULL DEFAULT 0, metadata_gen INTEGER NOT NULL DEFAULT 0,
restore_nonce BLOB,
transcript_ver INTEGER NOT NULL DEFAULT 1,
lock_pid INTEGER, lock_pid INTEGER,
lock_host TEXT, lock_host TEXT,
lock_time INTEGER lock_time INTEGER
@@ -2192,9 +2211,77 @@ impl Database {
self.canonical_nodes_bytes_for_vault(0) self.canonical_nodes_bytes_for_vault(0)
} }
/// Erzeugt die deterministische kanonische Byterepräsentation für einen spezifischen Vault. /// Erzeugt die deterministische kanonische Byterepräsentation für einen spezifischen Vault
/// inklusive sortierter Chunk-Generationen (Format V3.1 / F-01).
pub fn canonical_nodes_bytes_for_vault(&self, vault_id: u32) -> Result<Vec<u8>> { pub fn canonical_nodes_bytes_for_vault(&self, vault_id: u32) -> Result<Vec<u8>> {
let conn = self.conn(); Self::canonical_nodes_bytes_for_vault_on_conn(&self.conn(), vault_id)
}
/// Erzeugt die deterministische kanonische Byterepräsentation für einen spezifischen Vault
/// auf einer bestehenden SQLite-Verbindung oder Transaktion (F-05).
pub fn canonical_nodes_bytes_for_vault_on_conn(
conn: &rusqlite::Connection,
vault_id: u32,
) -> Result<Vec<u8>> {
let mut buf = Self::canonical_nodes_bytes_for_vault_legacy_on_conn(conn, vault_id)?;
if vault_id == 1 {
let mut stmt = conn.prepare(
"WITH RECURSIVE vault1(id) AS (
SELECT 2
UNION ALL
SELECT n.id FROM nodes n JOIN vault1 v ON n.parent_id = v.id
)
SELECT c.node_id, c.chunk_index, c.generation
FROM chunks c
WHERE c.node_id IN (SELECT id FROM vault1)
ORDER BY c.node_id ASC, c.chunk_index ASC",
)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let node_id: i64 = row.get(0)?;
let chunk_index: u32 = row.get(1)?;
let generation: u64 = row.get(2)?;
buf.extend_from_slice(&node_id.to_le_bytes());
buf.extend_from_slice(&chunk_index.to_le_bytes());
buf.extend_from_slice(&generation.to_le_bytes());
}
} else {
let mut stmt = conn.prepare(
"WITH RECURSIVE vault0(id) AS (
SELECT 1
UNION ALL
SELECT n.id FROM nodes n JOIN vault0 v ON n.parent_id = v.id
)
SELECT c.node_id, c.chunk_index, c.generation
FROM chunks c
WHERE c.node_id = 2 OR c.node_id IN (SELECT id FROM vault0)
ORDER BY c.node_id ASC, c.chunk_index ASC",
)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let node_id: i64 = row.get(0)?;
let chunk_index: u32 = row.get(1)?;
let generation: u64 = row.get(2)?;
buf.extend_from_slice(&node_id.to_le_bytes());
buf.extend_from_slice(&chunk_index.to_le_bytes());
buf.extend_from_slice(&generation.to_le_bytes());
}
}
Ok(buf)
}
/// Erzeugt die Legacy-Byterepräsentation ohne Chunk-Generationen (zur Migration bestehender V3-Container, F-01).
pub fn canonical_nodes_bytes_for_vault_legacy(&self, vault_id: u32) -> Result<Vec<u8>> {
Self::canonical_nodes_bytes_for_vault_legacy_on_conn(&self.conn(), vault_id)
}
/// Erzeugt die Legacy-Byterepräsentation ohne Chunk-Generationen auf einer bestehenden SQLite-Verbindung (F-05).
pub fn canonical_nodes_bytes_for_vault_legacy_on_conn(
conn: &rusqlite::Connection,
vault_id: u32,
) -> Result<Vec<u8>> {
let mut buf = Vec::new(); let mut buf = Vec::new();
if vault_id == 1 { if vault_id == 1 {
@@ -2271,7 +2358,7 @@ impl Database {
Ok(()) Ok(())
} }
/// Aktualisiert den Metadaten-MAC des aktiven Slots bei strukturellen Modifikationen (Format V3 / K-01). /// Aktualisiert den Metadaten-MAC des aktiven Slots bei strukturellen Modifikationen (Format V3 / K-01 & F-01).
pub fn update_metadata_mac(&self) -> Result<()> { pub fn update_metadata_mac(&self) -> Result<()> {
let session_opt = self let session_opt = self
.active_session .active_session
@@ -2304,11 +2391,11 @@ impl Database {
let next_gen = current_gen + 1; let next_gen = current_gen + 1;
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?; let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
let mac_key = derive_metadata_mac_key(&dek); let mac_key = derive_metadata_mac_key(&dek);
let new_mac = compute_metadata_mac(&mac_key, next_gen, &canonical); let new_mac = compute_metadata_mac_v2(&mac_key, next_gen, 2, &canonical);
let conn = self.conn(); let conn = self.conn();
conn.execute( conn.execute(
"UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2 WHERE slot_id = ?3", "UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2, transcript_ver = 2 WHERE slot_id = ?3",
params![new_mac.as_slice(), next_gen, slot_id], params![new_mac.as_slice(), next_gen, slot_id],
)?; )?;
@@ -2328,25 +2415,28 @@ impl Database {
/// Prüft die Integrität des Metadaten-MAC für einen spezifischen Slot (0: Decoy, 1: Hidden). /// Prüft die Integrität des Metadaten-MAC für einen spezifischen Slot (0: Decoy, 1: Hidden).
pub fn verify_metadata_mac_for_slot(&self, slot_id: u32, dek: &[u8; 32]) -> Result<bool> { pub fn verify_metadata_mac_for_slot(&self, slot_id: u32, dek: &[u8; 32]) -> Result<bool> {
Ok(self.verify_metadata_mac_status_for_slot(slot_id, dek)? == MetadataMacStatus::Valid) let status = self.verify_metadata_mac_status_for_slot(slot_id, dek)?;
Ok(status == MetadataMacStatus::Valid
|| status == MetadataMacStatus::LegacyValid
|| status == MetadataMacStatus::NeedsTranscriptVer)
} }
/// Prüft den detaillierten Integritätsstatus des Metadaten-MAC für einen spezifischen Slot (R-NEW-1). /// Prüft den detaillierten Integritätsstatus des Metadaten-MAC für einen spezifischen Slot (F-01 / F-02 / F-04).
pub fn verify_metadata_mac_status_for_slot( pub fn verify_metadata_mac_status_for_slot(
&self, &self,
slot_id: u32, slot_id: u32,
dek: &[u8; 32], dek: &[u8; 32],
) -> Result<MetadataMacStatus> { ) -> Result<MetadataMacStatus> {
let conn = self.conn(); let conn = self.conn();
let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn let meta_row: Option<(u32, Option<Vec<u8>>, u64, Option<Vec<u8>>, u32)> = conn
.query_row( .query_row(
"SELECT version, metadata_mac, metadata_gen FROM meta WHERE slot_id = ?1 LIMIT 1", "SELECT version, metadata_mac, metadata_gen, restore_nonce, transcript_ver FROM meta WHERE slot_id = ?1 LIMIT 1",
params![slot_id], params![slot_id],
|r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).unwrap_or(0))), |r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).unwrap_or(0), r.get(3).ok(), r.get(4).unwrap_or(1))),
) )
.optional()?; .optional()?;
let Some((version, mac_opt, gen)) = meta_row else { let Some((version, mac_opt, gen, restore_nonce, transcript_ver)) = meta_row else {
return Ok(MetadataMacStatus::Invalid); return Ok(MetadataMacStatus::Invalid);
}; };
@@ -2354,9 +2444,16 @@ impl Database {
return Ok(MetadataMacStatus::Valid); return Ok(MetadataMacStatus::Valid);
} }
// R-NEW-1: Frisch restaurierter Container (gen == 0 && mac_opt IS NULL) // F-02: Wenn metadata_mac fehlt (NULL):
if gen == 0 && mac_opt.is_none() { // NUR wenn restore_nonce vorhanden ist (32 Bytes) UND gen == 0,
return Ok(MetadataMacStatus::PendingRebuild); // ist der Status PendingRebuild. Andernfalls strikt Invalid (Fail-Closed, K-01 / F-02).
if mac_opt.is_none() {
if let Some(ref nonce) = restore_nonce {
if nonce.len() == 32 && gen == 0 {
return Ok(MetadataMacStatus::PendingRebuild);
}
}
return Ok(MetadataMacStatus::Invalid);
} }
let Some(mac_bytes) = mac_opt else { let Some(mac_bytes) = mac_opt else {
@@ -2371,25 +2468,61 @@ impl Database {
expected_mac.copy_from_slice(&mac_bytes); expected_mac.copy_from_slice(&mac_bytes);
drop(conn); drop(conn);
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
let mac_key = derive_metadata_mac_key(dek); let mac_key = derive_metadata_mac_key(dek);
if verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac) {
Ok(MetadataMacStatus::Valid) // F-04: Stufe 1: Rechne MAC mit ver=2 + canonical_v2 (0.9.5-native)
} else { // Treffer -> Valid. Fertig. Nie Legacy versuchen.
Ok(MetadataMacStatus::Invalid) let canonical_v2 = self.canonical_nodes_bytes_for_vault(slot_id)?;
if verify_metadata_mac_v2(&mac_key, gen, 2, &canonical_v2, &expected_mac) {
return Ok(MetadataMacStatus::Valid);
} }
// F-04: Wenn Stufe 1 fehlschlägt und der Container in SQLite bereits transcript_ver >= 2
// hat, ist kein Fallback auf Legacy erlaubt (Verhinderung von Replay-Angriffen nach Rebind).
if transcript_ver >= 2 {
return Ok(MetadataMacStatus::Invalid);
}
// F-04: Stufe 2: 0.9.4-native (ohne transcript_ver im Input, canonical_v2 mit Generationen)
if verify_metadata_mac(&mac_key, gen, &canonical_v2, &expected_mac) {
return Ok(MetadataMacStatus::NeedsTranscriptVer);
}
// F-04: Stufe 3: 0.9.3-native (ohne transcript_ver oder ver=1, canonical_legacy nur Knoten)
let canonical_legacy = self.canonical_nodes_bytes_for_vault_legacy(slot_id)?;
if verify_metadata_mac(&mac_key, gen, &canonical_legacy, &expected_mac)
|| verify_metadata_mac_v2(&mac_key, gen, 1, &canonical_legacy, &expected_mac)
{
return Ok(MetadataMacStatus::LegacyValid);
}
Ok(MetadataMacStatus::Invalid)
} }
/// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01 & K-02). /// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01, K-02 & F-03).
pub fn upgrade_to_v3(&self, dek: &[u8; 32]) -> Result<()> { pub fn upgrade_to_v3(&self, dek: &[u8; 32]) -> Result<()> {
let conn = self.conn(); self.upgrade_to_v3_ext(dek, None)
let version: u32 = conn.query_row( }
"SELECT version FROM meta WHERE slot_id = 0 LIMIT 1",
[], /// Führt ein transaktionales Upgrade des Containerformats auf Format V3 durch (F-03 / F-05).
|r| r.get(0), /// Läuft in einer einzigen atomaren SQLite-Transaktion. Sowohl die Re-Verschlüsselung der Chunks,
)?; /// das Setzen von version = 3 als auch die Berechnung und Speicherung des Metadaten-MACs (transcript_ver = 2)
/// erfolgen vor dem Commit (F-05). Bei jeglichem Chunk-Fehler wird ein Rollback durchgeführt.
pub fn upgrade_to_v3_ext(&self, dek_0: &[u8; 32], dek_1: Option<&[u8; 32]>) -> Result<()> {
let mut conn = self.conn();
let (version, mac_opt, nonce_opt): (u32, Option<Vec<u8>>, Option<Vec<u8>>) = conn
.query_row(
"SELECT version, metadata_mac, restore_nonce FROM meta WHERE slot_id = 0 LIMIT 1",
[],
|r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).ok())),
)?;
if version >= FORMAT_VERSION_V3 { if version >= FORMAT_VERSION_V3 {
// F-05: Zombie-Container Reparatur (V3, MAC NULL, restore_nonce NULL)
if mac_opt.is_none() && nonce_opt.is_none() {
drop(conn);
return self.repair_v3_mac(dek_0, dek_1);
}
return Ok(()); return Ok(());
} }
@@ -2398,15 +2531,23 @@ impl Database {
"ALTER TABLE meta ADD COLUMN metadata_gen INTEGER NOT NULL DEFAULT 0", "ALTER TABLE meta ADD COLUMN metadata_gen INTEGER NOT NULL DEFAULT 0",
[], [],
); );
let _ = conn.execute("ALTER TABLE meta ADD COLUMN restore_nonce BLOB", []);
let _ = conn.execute(
"ALTER TABLE meta ADD COLUMN transcript_ver INTEGER NOT NULL DEFAULT 1",
[],
);
let _ = conn.execute( let _ = conn.execute(
"ALTER TABLE chunks ADD COLUMN generation INTEGER NOT NULL DEFAULT 0", "ALTER TABLE chunks ADD COLUMN generation INTEGER NOT NULL DEFAULT 0",
[], [],
); );
// K-02: Alle bestehenden Chunks von alter 16-Byte-AAD auf Format V3 24-Byte-AAD (generation = 0) umverschlüsseln // F-03 / F-05: Gesamte Migration inkl. MAC-Persistierung in einer einzigen atomaren SQLite-Transaktion
let tx = conn.transaction()?;
// Alle bestehenden Chunks von alter 16-Byte-AAD auf Format V3 24-Byte-AAD (generation = 0) umverschlüsseln
{ {
let mut chunk_stmt = let mut chunk_stmt =
conn.prepare("SELECT node_id, chunk_index, nonce, tag, ciphertext FROM chunks")?; tx.prepare("SELECT node_id, chunk_index, nonce, tag, ciphertext FROM chunks")?;
let chunk_rows: Vec<(i64, u32, [u8; 12], [u8; 16], Vec<u8>)> = chunk_stmt let chunk_rows: Vec<(i64, u32, [u8; 12], [u8; 16], Vec<u8>)> = chunk_stmt
.query_map([], |row| { .query_map([], |row| {
let node_id: i64 = row.get(0)?; let node_id: i64 = row.get(0)?;
@@ -2428,8 +2569,8 @@ impl Database {
drop(chunk_stmt); drop(chunk_stmt);
for (node_id, chunk_index, nonce, tag, ct) in chunk_rows { for (node_id, chunk_index, nonce, tag, ct) in chunk_rows {
if let Ok(plaintext) = crate::crypto::decrypt_chunk( let (plaintext, chunk_dek) = match crate::crypto::decrypt_chunk(
dek, dek_0,
node_id, node_id,
chunk_index, chunk_index,
&ct, &ct,
@@ -2438,42 +2579,176 @@ impl Database {
version, version,
0, 0,
) { ) {
if let Ok((new_ct, new_nonce, new_tag)) = crate::crypto::encrypt_chunk( Ok(pt) => (pt, dek_0),
dek, Err(e0) => {
node_id, if let Some(d1) = dek_1 {
chunk_index, match crate::crypto::decrypt_chunk(
&plaintext, d1,
FORMAT_VERSION_V3,
0,
) {
conn.execute(
"UPDATE chunks SET nonce = ?1, tag = ?2, ciphertext = ?3, generation = 0 WHERE node_id = ?4 AND chunk_index = ?5",
params![
new_nonce.as_slice(),
new_tag.as_slice(),
new_ct,
node_id, node_id,
chunk_index chunk_index,
], &ct,
)?; &nonce,
&tag,
version,
0,
) {
Ok(pt) => (pt, d1),
Err(_) => {
bail!(
"Upgrade auf Format V3 abgebrochen: Chunk (Node {}, Index {}) konnte weder mit DEK_0 noch mit DEK_1 entschlüsselt werden.",
node_id, chunk_index
);
}
}
} else {
bail!(
"Upgrade auf Format V3 abgebrochen: Chunk (Node {}, Index {}) konnte mit DEK_0 nicht entschlüsselt werden ({}). Falls dieser Container einen Legacy-Hidden-Vault enthält, wird das zweite Passwort benötigt.",
node_id, chunk_index, e0
);
}
} }
} };
let (new_ct, new_nonce, new_tag) = crate::crypto::encrypt_chunk(
chunk_dek,
node_id,
chunk_index,
&plaintext,
FORMAT_VERSION_V3,
0,
)?;
tx.execute(
"UPDATE chunks SET nonce = ?1, tag = ?2, ciphertext = ?3, generation = 0 WHERE node_id = ?4 AND chunk_index = ?5",
params![
new_nonce.as_slice(),
new_tag.as_slice(),
new_ct,
node_id,
chunk_index
],
)?;
} }
} }
conn.execute( // F-05: In derselben Transaktion vor dem Commit: Metadaten-MAC berechnen und persistieren
"UPDATE meta SET version = ?1, metadata_gen = 0 WHERE slot_id = 0", let canonical_0 = Self::canonical_nodes_bytes_for_vault_on_conn(&tx, 0)?;
[FORMAT_VERSION_V3], let mac_key_0 = derive_metadata_mac_key(dek_0);
let mac_0 = compute_metadata_mac_v2(&mac_key_0, 1, 2, &canonical_0);
tx.execute(
"UPDATE meta SET version = ?1, metadata_mac = ?2, metadata_gen = 1, transcript_ver = 2 WHERE slot_id = 0",
params![FORMAT_VERSION_V3, mac_0.as_slice()],
)?; )?;
let has_vault1_nodes: bool = tx
.query_row(
"SELECT 1 FROM nodes WHERE parent_id = 2 LIMIT 1",
[],
|_| Ok(true),
)
.optional()?
.unwrap_or(false);
if has_vault1_nodes {
if let Some(d1) = dek_1 {
let canonical_1 = Self::canonical_nodes_bytes_for_vault_on_conn(&tx, 1)?;
let mac_key_1 = derive_metadata_mac_key(d1);
let mac_1 = compute_metadata_mac_v2(&mac_key_1, 1, 2, &canonical_1);
tx.execute(
"UPDATE meta SET version = ?1, metadata_mac = ?2, metadata_gen = 1, transcript_ver = 2 WHERE slot_id = 1",
params![FORMAT_VERSION_V3, mac_1.as_slice()],
)?;
}
} else {
tx.execute(
"UPDATE meta SET version = ?1, metadata_gen = 0, transcript_ver = 2 WHERE slot_id != 0 AND (metadata_mac IS NULL OR slot_id = 1)",
params![FORMAT_VERSION_V3],
)?;
}
tx.commit()?;
drop(conn); drop(conn);
self.set_active_dek(Zeroizing::new(*dek)); // Aktive Session auf Slot 0 setzen
self.update_metadata_mac()?; self.set_active_slot_and_dek(0, Zeroizing::new(*dek_0));
Ok(()) Ok(())
} }
/// Repariert einen unvollständigen Zombie-Container (version >= 3, metadata_mac IS NULL, restore_nonce IS NULL) (F-05).
/// Bricht strikt ab, falls restore_nonce gesetzt ist (dies ist F-02 vorbehalten).
pub fn repair_v3_mac(&self, dek_0: &[u8; 32], dek_1: Option<&[u8; 32]>) -> Result<()> {
let mut conn = self.conn();
let (version, mac_opt, nonce_opt): (u32, Option<Vec<u8>>, Option<Vec<u8>>) = conn
.query_row(
"SELECT version, metadata_mac, restore_nonce FROM meta WHERE slot_id = 0 LIMIT 1",
[],
|r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).ok())),
)?;
if version < FORMAT_VERSION_V3 {
bail!("Reparatur abgebrochen: Container ist nicht Format V3.");
}
if nonce_opt.is_some() {
bail!(
"Reparatur abgebrochen: Container enthält restore_nonce Token. Starten Sie 'sanctum mount --rebuild-mac' (F-02)."
);
}
if mac_opt.is_some() {
return Ok(());
}
let _ = conn.execute(
"ALTER TABLE meta ADD COLUMN transcript_ver INTEGER NOT NULL DEFAULT 1",
[],
);
let tx = conn.transaction()?;
let canonical_0 = Self::canonical_nodes_bytes_for_vault_on_conn(&tx, 0)?;
let mac_key_0 = derive_metadata_mac_key(dek_0);
let mac_0 = compute_metadata_mac_v2(&mac_key_0, 1, 2, &canonical_0);
tx.execute(
"UPDATE meta SET version = ?1, metadata_mac = ?2, metadata_gen = 1, transcript_ver = 2 WHERE slot_id = 0",
params![FORMAT_VERSION_V3, mac_0.as_slice()],
)?;
let has_vault1_nodes: bool = tx
.query_row(
"SELECT 1 FROM nodes WHERE parent_id = 2 LIMIT 1",
[],
|_| Ok(true),
)
.optional()?
.unwrap_or(false);
if has_vault1_nodes {
if let Some(d1) = dek_1 {
let canonical_1 = Self::canonical_nodes_bytes_for_vault_on_conn(&tx, 1)?;
let mac_key_1 = derive_metadata_mac_key(d1);
let mac_1 = compute_metadata_mac_v2(&mac_key_1, 1, 2, &canonical_1);
tx.execute(
"UPDATE meta SET version = ?1, metadata_mac = ?2, metadata_gen = 1, transcript_ver = 2 WHERE slot_id = 1",
params![FORMAT_VERSION_V3, mac_1.as_slice()],
)?;
}
} else {
tx.execute(
"UPDATE meta SET version = ?1, metadata_gen = 0, transcript_ver = 2 WHERE slot_id != 0 AND (metadata_mac IS NULL OR slot_id = 1)",
params![FORMAT_VERSION_V3],
)?;
}
tx.commit()?;
drop(conn);
self.set_active_slot_and_dek(0, Zeroizing::new(*dek_0));
Ok(())
}
/// Erzwingt einen SQLite WAL Checkpoint und leert das Write-Ahead-Log. /// Erzwingt einen SQLite WAL Checkpoint und leert das Write-Ahead-Log.
pub fn checkpoint(&self) -> Result<()> { pub fn checkpoint(&self) -> Result<()> {
let conn = self.conn(); let conn = self.conn();
@@ -2542,6 +2817,15 @@ impl Database {
/// Schreibt oder stellt die Metadaten in der `meta`-Tabelle wieder her (z. B. nach Restore oder Header-Neugenerierung). /// Schreibt oder stellt die Metadaten in der `meta`-Tabelle wieder her (z. B. nach Restore oder Header-Neugenerierung).
pub fn restore_meta(&self, meta: &ContainerMeta) -> Result<()> { pub fn restore_meta(&self, meta: &ContainerMeta) -> Result<()> {
self.restore_meta_with_nonce(meta, None)
}
/// Schreibt oder stellt die Metadaten in der `meta`-Tabelle mit optionalem restore_nonce Token wieder her (F-02).
pub fn restore_meta_with_nonce(
&self,
meta: &ContainerMeta,
restore_nonce: Option<&[u8; 32]>,
) -> Result<()> {
let conn = self.conn(); let conn = self.conn();
conn.execute_batch( conn.execute_batch(
@@ -2556,6 +2840,8 @@ impl Database {
header_tag BLOB NOT NULL, header_tag BLOB NOT NULL,
metadata_mac BLOB, metadata_mac BLOB,
metadata_gen INTEGER NOT NULL DEFAULT 0, metadata_gen INTEGER NOT NULL DEFAULT 0,
restore_nonce BLOB,
transcript_ver INTEGER NOT NULL DEFAULT 1,
lock_pid INTEGER, lock_pid INTEGER,
lock_host TEXT, lock_host TEXT,
lock_time INTEGER lock_time INTEGER
@@ -2564,6 +2850,8 @@ impl Database {
conn.execute("DELETE FROM meta", [])?; conn.execute("DELETE FROM meta", [])?;
let nonce_bytes = restore_nonce.map(|n| n.as_slice());
let mut has_slot1 = false; let mut has_slot1 = false;
if !meta.slots.is_empty() { if !meta.slots.is_empty() {
for slot in &meta.slots { for slot in &meta.slots {
@@ -2572,8 +2860,8 @@ impl Database {
} }
let params_json = serde_json::to_string(&slot.kdf_params)?; let params_json = serde_json::to_string(&slot.kdf_params)?;
conn.execute( conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen) "INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen, restore_nonce)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, 0)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, 0, ?9)",
params![ params![
slot.slot_id, slot.slot_id,
MAGIC_BYTES.as_slice(), MAGIC_BYTES.as_slice(),
@@ -2583,14 +2871,15 @@ impl Database {
slot.wrapped_dek, slot.wrapped_dek,
slot.header_nonce.as_slice(), slot.header_nonce.as_slice(),
slot.header_tag.as_slice(), slot.header_tag.as_slice(),
nonce_bytes,
], ],
)?; )?;
} }
} else { } else {
let params_json = serde_json::to_string(&meta.kdf_params)?; let params_json = serde_json::to_string(&meta.kdf_params)?;
conn.execute( conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen) "INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen, restore_nonce)
VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0)", VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0, ?8)",
params![ params![
MAGIC_BYTES.as_slice(), MAGIC_BYTES.as_slice(),
meta.version, meta.version,
@@ -2599,6 +2888,7 @@ impl Database {
meta.wrapped_dek, meta.wrapped_dek,
meta.header_nonce.as_slice(), meta.header_nonce.as_slice(),
meta.header_tag.as_slice(), meta.header_tag.as_slice(),
nonce_bytes,
], ],
)?; )?;
} }
@@ -2608,8 +2898,8 @@ impl Database {
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot(); let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
let dummy_params_json = serde_json::to_string(&KdfParams::default())?; let dummy_params_json = serde_json::to_string(&KdfParams::default())?;
conn.execute( conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen) "INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen, restore_nonce)
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0)", VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0, ?8)",
params![ params![
MAGIC_BYTES.as_slice(), MAGIC_BYTES.as_slice(),
FORMAT_VERSION, FORMAT_VERSION,
@@ -2618,6 +2908,7 @@ impl Database {
dummy_dek.as_slice(), dummy_dek.as_slice(),
dummy_nonce.as_slice(), dummy_nonce.as_slice(),
dummy_tag.as_slice(), dummy_tag.as_slice(),
nonce_bytes,
], ],
)?; )?;
} }
@@ -2625,6 +2916,16 @@ impl Database {
Ok(()) Ok(())
} }
/// Löscht das Restore-Token nach erfolgreichem MAC-Rebuild (F-02).
pub fn clear_restore_nonce(&self, slot_id: u32) -> Result<()> {
let conn = self.conn();
conn.execute(
"UPDATE meta SET restore_nonce = NULL WHERE slot_id = ?1",
params![slot_id],
)?;
Ok(())
}
/// Führt SQLite-eigene Integritäts- und Foreign-Key-Prüfungen aus. /// Führt SQLite-eigene Integritäts- und Foreign-Key-Prüfungen aus.
pub fn run_sqlite_integrity_check(&self) -> Result<Vec<String>> { pub fn run_sqlite_integrity_check(&self) -> Result<Vec<String>> {
let conn = self.conn(); let conn = self.conn();
File diff suppressed because it is too large Load Diff
+3
View File
@@ -375,6 +375,7 @@ async fn test_mount_rejects_tampered_metadata_mac() {
false, false,
None, None,
true, true,
false,
) )
.await; .await;
@@ -472,6 +473,7 @@ async fn test_mount_succeeds_and_rebuilds_mac_after_header_file_restore() {
false, false,
None, None,
true, true,
true,
) )
.await .await
}); });
@@ -577,6 +579,7 @@ async fn test_mount_succeeds_after_recovery_key_restore_slot0() {
false, false,
None, None,
true, true,
false,
) )
.await .await
}); });