Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba7f305e3 |
@@ -6,16 +6,18 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# SA-02 & SA-04: Entkoppelte Build- & Test-Umgebung ohne Zugriff auf Signatur-Secrets
|
||||||
build:
|
build:
|
||||||
name: Build & Release (Windows x86_64)
|
name: Build & Test (Windows x86_64)
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master commit pinned
|
||||||
with:
|
with:
|
||||||
|
toolchain: "1.85.0"
|
||||||
targets: x86_64-pc-windows-msvc
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
@@ -47,21 +49,54 @@ jobs:
|
|||||||
|
|
||||||
@("$ZipHash ${PackageName}.zip", "$ExeHash sanctum.exe") | Set-Content -Path "${DistDir}/SHA256SUMS.txt" -Encoding utf8
|
@("$ZipHash ${PackageName}.zip", "$ExeHash sanctum.exe") | Set-Content -Path "${DistDir}/SHA256SUMS.txt" -Encoding utf8
|
||||||
|
|
||||||
|
|
||||||
echo "ZIP_FILE=$ZipFile" >> $env:GITHUB_OUTPUT
|
echo "ZIP_FILE=$ZipFile" >> $env:GITHUB_OUTPUT
|
||||||
echo "PACKAGE_NAME=$PackageName" >> $env:GITHUB_OUTPUT
|
echo "PACKAGE_NAME=$PackageName" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Sign Checksums with Minisign
|
- name: Upload Build Artifacts
|
||||||
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
|
with:
|
||||||
|
name: dist-artifacts
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
# SA-02, SA-03 & SA-04: Dedizierter Signatur- & Release-Job mit isolierten Secrets und Prüfsummen-Verifikation
|
||||||
|
sign-and-release:
|
||||||
|
name: Sign & Release
|
||||||
|
needs: [build]
|
||||||
|
runs-on: windows-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: Download Build Artifacts
|
||||||
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
|
with:
|
||||||
|
name: dist-artifacts
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Verify and Sign Checksums with Minisign
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
|
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
|
||||||
run: |
|
run: |
|
||||||
|
# SA-02: Secret-Isolation — MINISIGN_SECRET_KEY ist ausschließlich in diesem dedizierten Signier-Job verfügbar
|
||||||
if (-not $env:MINISIGN_SECRET_KEY) {
|
if (-not $env:MINISIGN_SECRET_KEY) {
|
||||||
Write-Error "Secret MINISIGN_SECRET_KEY ist nicht gesetzt! Release ohne Signatur verboten."
|
Write-Error "Secret MINISIGN_SECRET_KEY ist nicht gesetzt! Release ohne Signatur verboten."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# SA-03: Pinned Download-Integrität für minisign.exe mit zwingender SHA-256 Validierung
|
||||||
|
$MinisignUrl = "https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-win64.zip"
|
||||||
|
$ExpectedMinisignHash = "b9c31c2c3034f81f0e5f5d92cbcc20e67a9671b6e5455661588638848dc58031"
|
||||||
|
|
||||||
if (-not (Get-Command minisign -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command minisign -ErrorAction SilentlyContinue)) {
|
||||||
Invoke-WebRequest -Uri "https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-win64.zip" -OutFile "minisign.zip"
|
Write-Host "Lade gepinnten Minisign-Release herunter: $MinisignUrl"
|
||||||
|
Invoke-WebRequest -Uri $MinisignUrl -OutFile "minisign.zip"
|
||||||
|
|
||||||
|
$ActualHash = (Get-FileHash -Path "minisign.zip" -Algorithm SHA256).Hash.ToLower()
|
||||||
|
if ($ActualHash -ne $ExpectedMinisignHash) {
|
||||||
|
Write-Error "KRITISCHER SICHERHEITSFEHLER: Minisign SHA-256 Prüfsumme ungültig!`nErwartet: $ExpectedMinisignHash`nErhalten: $ActualHash`nAbbruch."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Minisign SHA-256 Integrität erfolgreich verifiziert ($ActualHash)."
|
||||||
|
|
||||||
Expand-Archive -Path "minisign.zip" -DestinationPath "minisign-bin"
|
Expand-Archive -Path "minisign.zip" -DestinationPath "minisign-bin"
|
||||||
$MinisignExe = "minisign-bin/minisign-win64/minisign.exe"
|
$MinisignExe = "minisign-bin/minisign-win64/minisign.exe"
|
||||||
} else {
|
} else {
|
||||||
@@ -81,14 +116,12 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: Create Gitea Release
|
- name: Create Gitea Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/SHA256SUMS.txt
|
dist/SHA256SUMS.txt
|
||||||
dist/SHA256SUMS.txt.minisig
|
dist/SHA256SUMS.txt.minisig
|
||||||
target/release/sanctum.exe
|
|
||||||
body_path: CHANGELOG.md
|
body_path: CHANGELOG.md
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|||||||
@@ -5,6 +5,32 @@ 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.7.2] - 2026-09-18
|
||||||
|
|
||||||
|
### Security Audit Remediation Release (Findings SA-01 to SA-07)
|
||||||
|
Dieses Release behebt alle sieben identifizierten Schwachstellen und Härtungsanforderungen (SA-01 bis SA-07) aus dem Security-Audit von v0.7.1.
|
||||||
|
|
||||||
|
#### Security & Architecture
|
||||||
|
- **SA-01: Container-DoS / KDF-Amplification Schutz**:
|
||||||
|
- Pre-KDF strukturelle Validierung in `Database::open`, `read_slots`, `ContainerMeta::authenticate` und `recovery::to_meta`.
|
||||||
|
- Striktes Limit: Maximal 2 Slots (`slots.len() <= 2`), nur Slot-IDs 0 und 1 zulässig, Erkennung von Duplikaten, Slot 0 ist Pflicht.
|
||||||
|
- Strikte Validierung aller BLOB-Längen (Salt: 16 B, Nonce: 12 B, Tag: 16 B, Wrapped DEK: 32/40 B für Slot 0, 32/64/72 B für Slot 1) und KDF-Parametergrenzen vor jeder Argon2id-Ableitung (`derive_kek`).
|
||||||
|
- **SA-02: Release-Signierung entkoppelt & CI-Secrets isoliert**:
|
||||||
|
- Trennung des CI/CD-Release-Workflows (`release.yaml`) in separate `build`- und `sign-and-release`-Jobs.
|
||||||
|
- Das Secret `MINISIGN_SECRET_KEY` ist strikt auf den nachgelagerten Signatur-Job isoliert und für Build-/Test-Runner unzugänglich.
|
||||||
|
- **SA-03: Pinned Download-Integrität für `minisign.exe`**:
|
||||||
|
- SHA-256 Integritätsprüfung des offiziellen Minisign v0.11 Downloads (`b9c31c2c3034f81f0e5f5d92cbcc20e67a9671b6e5455661588638848dc58031`) im CI-Workflow vor dem Entpacken und Ausführen (Fail-Closed).
|
||||||
|
- **SA-04: Immutable Action-Pinning & Toolchain-Pinning**:
|
||||||
|
- Alle externen GitHub/Gitea-Actions im Release-Workflow auf unveränderliche Commit-SHAs (`@<sha>`) fixiert.
|
||||||
|
- Rust-Toolchain explizit auf Version `1.85.0` gepinnt.
|
||||||
|
- **SA-05: Session-Tokens vollständig aus URIs verbannt**:
|
||||||
|
- Eingehende WebDAV-Requests, deren URI das Session-Token in Pfad oder Query-String enthalten (`/<token>/...` oder `?token=...`), werden sofort mit `403 Forbidden` abgewiesen.
|
||||||
|
- Authentifizierung wird strikt über HTTP-Header (`Authorization: Basic ...` oder `X-Sanctum-Token: ...`) erzwungen.
|
||||||
|
- **SA-06: Constant-Time Token-Vergleich**:
|
||||||
|
- Integration von `subtle::ConstantTimeEq` für zeitinvariante Vergleiche von Basic-Auth-Credentials und Session-Tokens zum Schutz vor Timing-Seitenkanalangriffen.
|
||||||
|
- **SA-07: Dokumentations-Klarstellung: Logische vs. Physische Datenlöschung**:
|
||||||
|
- Präzisierung im `README.md` und Code-Docstrings bzgl. der Grenzen des kryptografischen Chunk-Shreddings: Auf Flash-Medien (SSDs/NVMe) können Wear-Leveling und FTL-Blöcke physisch persistieren. Empfehlung zur Nutzung von Full-Disk-Encryption (FDE).
|
||||||
|
|
||||||
## [0.7.1] - 2026-09-18
|
## [0.7.1] - 2026-09-18
|
||||||
|
|
||||||
### Security Patch Release (Closing Gaps R-01 to R-06)
|
### Security Patch Release (Closing Gaps R-01 to R-06)
|
||||||
|
|||||||
Generated
+2
-1
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1483,6 +1483,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"subtle",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
||||||
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
||||||
@@ -42,6 +42,7 @@ ureq = { version = "2.10", default-features = false, features = ["json", "tls"]
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
semver = "1.0"
|
semver = "1.0"
|
||||||
minisign-verify = "0.2"
|
minisign-verify = "0.2"
|
||||||
|
subtle = "2.6"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ Sanctum ist eine eigenständige, speichersichere und hochperformante CLI-Anwendu
|
|||||||
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).
|
||||||
- **Dateinamen-Verschlüsselung**:
|
- **Dateinamen-Verschlüsselung**:
|
||||||
Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert (Legacy-Kompatibilität über `--legacy-names`).
|
Dateinamen im Hidden Vault werden mit frischen CSPRNG-Nonces und AES-256-GCM verschlüsselt in der Datenbank gespeichert (Legacy-Kompatibilität über `--legacy-names`).
|
||||||
- **Kryptografisches Chunk-Shredding**:
|
- **Kryptografisches Chunk-Shredding (Logisches Löschen vs. Physikalische Bereinigung)**:
|
||||||
Vor jedem Löschen oder Kürzen werden Chunk-Payloads in der SQLite-Datenbank mit CSPRNG-Rauschen überschrieben.
|
Vor jedem Löschen oder Kürzen werden Chunk-Payloads, Nonces und Tags in der SQLite-Datenbank transaktional mit CSPRNG-Zufallsrauschen überschrieben. Dies verhindert zuverlässig jede logische Rekonstruktion auf Datenbank- und Dateisystemebene.
|
||||||
|
*Wichtiger technischer Hinweis (SA-07)*: Dies stellt ein *logisches* sicheres Löschen dar. Auf modernen Solid-State-Drives (SSD, NVMe) und Copy-on-Write-Dateisystemen (Btrfs, ZFS, APFS, ReFS) kann Software im Userland bauartbedingt keine *physikalische* Datenträgerbereinigung (Media Sanitization) garantieren: Der Flash Translation Layer (FTL) und Wear-Leveling-Algorithmen leiten Schreiboperationen auf neue Flash-Blöcke um; alte physikalische Zellen verbleiben bis zur SSD Garbage Collection / TRIM im Flash. Für absolute physische Bereinigung wird eine hardware- oder blockebenenbasierte Vollverschlüsselung (BitLocker, LUKS) oder ein ATA/NVMe Secure Erase empfohlen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+11
-18
@@ -10,8 +10,8 @@ use bytes::{Buf, Bytes, BytesMut};
|
|||||||
use dav_server::{
|
use dav_server::{
|
||||||
davpath::DavPath,
|
davpath::DavPath,
|
||||||
fs::{
|
fs::{
|
||||||
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsStream,
|
DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError, FsFuture, FsStream, OpenOptions,
|
||||||
OpenOptions, ReadDirMeta,
|
ReadDirMeta,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use futures_util::stream;
|
use futures_util::stream;
|
||||||
@@ -624,9 +624,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
};
|
};
|
||||||
|
|
||||||
inner.manifest.inodes.insert(new_id, new_dir);
|
inner.manifest.inodes.insert(new_id, new_dir);
|
||||||
inner
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
||||||
.save_manifest()
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -659,9 +657,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner.manifest.inodes.remove(&node.id);
|
inner.manifest.inodes.remove(&node.id);
|
||||||
inner
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
||||||
.save_manifest()
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -690,9 +686,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner.manifest.inodes.remove(&node.id);
|
inner.manifest.inodes.remove(&node.id);
|
||||||
inner
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
||||||
.save_manifest()
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -748,9 +742,7 @@ impl DavFileSystem for CarrierFs {
|
|||||||
inode.modified_at = now;
|
inode.modified_at = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
inner
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
||||||
.save_manifest()
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -1011,9 +1003,7 @@ impl DavFile for CarrierFile {
|
|||||||
inode.modified_at = now;
|
inode.modified_at = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
inner
|
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
|
||||||
.save_manifest()
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
self.meta.size = self.file_size;
|
self.meta.size = self.file_size;
|
||||||
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||||
@@ -1025,7 +1015,10 @@ impl DavFile for CarrierFile {
|
|||||||
impl Drop for CarrierFile {
|
impl Drop for CarrierFile {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Err(e) = self.flush_cached_block() {
|
if let Err(e) = self.flush_cached_block() {
|
||||||
tracing::warn!("Fehler beim automatischen Flush im CarrierFile::drop: {:?}", e);
|
tracing::warn!(
|
||||||
|
"Fehler beim automatischen Flush im CarrierFile::drop: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some((_, ref mut data, _)) = self.cached_block {
|
if let Some((_, ref mut data, _)) = self.cached_block {
|
||||||
data.zeroize();
|
data.zeroize();
|
||||||
|
|||||||
+185
-63
@@ -101,11 +101,7 @@ pub fn check_password_prefix_collision(pass0: &str, pass1: &str) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Leitet aus dem Master-Passwort und dem Salt einen 256-Bit Key Encryption Key (KEK) via Argon2id ab.
|
/// Leitet aus dem Master-Passwort und dem Salt einen 256-Bit Key Encryption Key (KEK) via Argon2id ab.
|
||||||
pub fn derive_kek(
|
pub fn derive_kek(password: &str, salt: &[u8], params: &KdfParams) -> Result<Zeroizing<[u8; 32]>> {
|
||||||
password: &str,
|
|
||||||
salt: &[u8],
|
|
||||||
params: &KdfParams,
|
|
||||||
) -> Result<Zeroizing<[u8; 32]>> {
|
|
||||||
validate_kdf_params(params)?;
|
validate_kdf_params(params)?;
|
||||||
|
|
||||||
let argon2_params = Params::new(
|
let argon2_params = Params::new(
|
||||||
@@ -145,10 +141,7 @@ pub fn generate_salt() -> [u8; 16] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verschlüsselt beliebige Schlüsseldaten (32B DEK, 40B Slot0-Payload oder 72B Slot1-Payload) via AES-256-GCM.
|
/// Verschlüsselt beliebige Schlüsseldaten (32B DEK, 40B Slot0-Payload oder 72B Slot1-Payload) via AES-256-GCM.
|
||||||
pub fn wrap_key_payload(
|
pub fn wrap_key_payload(kek: &[u8; 32], payload: &[u8]) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||||
kek: &[u8; 32],
|
|
||||||
payload: &[u8],
|
|
||||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
|
||||||
let cipher = Aes256Gcm::new_from_slice(kek)
|
let cipher = Aes256Gcm::new_from_slice(kek)
|
||||||
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("AES-GCM Initialisierungsfehler: {e}"))?;
|
||||||
|
|
||||||
@@ -183,17 +176,18 @@ pub fn unwrap_key_payload(
|
|||||||
let mut buffer = wrapped_payload.to_vec();
|
let mut buffer = wrapped_payload.to_vec();
|
||||||
cipher
|
cipher
|
||||||
.decrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer, tag)
|
.decrypt_in_place_detached(nonce, b"SANCTUM_HEADER_DEK", &mut buffer, tag)
|
||||||
.map_err(|_| anyhow::anyhow!("Passwort falsch oder Header beschädigt (AEAD Authentifizierungsfehler)"))?;
|
.map_err(|_| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Passwort falsch oder Header beschädigt (AEAD Authentifizierungsfehler)"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Zeroizing::new(buffer))
|
Ok(Zeroizing::new(buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verschlüsselt den DEK (32 Bytes) mit dem KEK via AES-256-GCM.
|
/// Verschlüsselt den DEK (32 Bytes) mit dem KEK via AES-256-GCM.
|
||||||
/// Gibt (wrapped_dek_32_bytes, nonce_12_bytes, tag_16_bytes) zurück.
|
/// Gibt (wrapped_dek_32_bytes, nonce_12_bytes, tag_16_bytes) zurück.
|
||||||
pub fn wrap_dek(
|
pub fn wrap_dek(kek: &[u8; 32], dek: &[u8; 32]) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
||||||
kek: &[u8; 32],
|
|
||||||
dek: &[u8; 32],
|
|
||||||
) -> Result<(Vec<u8>, [u8; 12], [u8; 16])> {
|
|
||||||
wrap_key_payload(kek, dek)
|
wrap_key_payload(kek, dek)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +200,10 @@ pub fn unwrap_dek(
|
|||||||
) -> Result<Zeroizing<[u8; 32]>> {
|
) -> Result<Zeroizing<[u8; 32]>> {
|
||||||
let payload = unwrap_key_payload(kek, wrapped_dek, nonce_bytes, tag_bytes)?;
|
let payload = unwrap_key_payload(kek, wrapped_dek, nonce_bytes, tag_bytes)?;
|
||||||
if payload.len() < 32 {
|
if payload.len() < 32 {
|
||||||
bail!("Ungültige wrapped_dek Länge: erwartet mindestens 32 Bytes, erhalten {}", payload.len());
|
bail!(
|
||||||
|
"Ungültige wrapped_dek Länge: erwartet mindestens 32 Bytes, erhalten {}",
|
||||||
|
payload.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut dek = Zeroizing::new([0u8; 32]);
|
let mut dek = Zeroizing::new([0u8; 32]);
|
||||||
@@ -265,7 +262,8 @@ pub fn build_name_aad(parent_id: i64) -> [u8; 16] {
|
|||||||
aad
|
aad
|
||||||
}
|
}
|
||||||
|
|
||||||
static ALLOW_LEGACY_NAMES: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
static ALLOW_LEGACY_NAMES: std::sync::atomic::AtomicBool =
|
||||||
|
std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
/// Aktiviert oder deaktiviert den veralteten AAD-Fallback für Dateinamen (S-10).
|
/// Aktiviert oder deaktiviert den veralteten AAD-Fallback für Dateinamen (S-10).
|
||||||
pub fn set_allow_legacy_names(allow: bool) {
|
pub fn set_allow_legacy_names(allow: bool) {
|
||||||
@@ -289,7 +287,11 @@ pub fn encrypt_node_name(dek: &[u8; 32], parent_id: i64, name: &str) -> String {
|
|||||||
};
|
};
|
||||||
let mut buffer = name.as_bytes().to_vec();
|
let mut buffer = name.as_bytes().to_vec();
|
||||||
let aad = build_name_aad(parent_id);
|
let aad = build_name_aad(parent_id);
|
||||||
let tag = match cipher.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), &aad, &mut buffer) {
|
let tag = match cipher.encrypt_in_place_detached(
|
||||||
|
Nonce::from_slice(&nonce_bytes),
|
||||||
|
&aad,
|
||||||
|
&mut buffer,
|
||||||
|
) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(_) => return String::new(),
|
Err(_) => return String::new(),
|
||||||
};
|
};
|
||||||
@@ -303,7 +305,12 @@ pub fn encrypt_node_name(dek: &[u8; 32], parent_id: i64, name: &str) -> String {
|
|||||||
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
|
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
|
||||||
/// Prüft primär die kryptografische Bindung an parent_id; bietet optional Fallback
|
/// Prüft primär die kryptografische Bindung an parent_id; bietet optional Fallback
|
||||||
/// auf die statische AAD für ältere Container, wenn `allow_legacy` aktiv ist (S-10).
|
/// auf die statische AAD für ältere Container, wenn `allow_legacy` aktiv ist (S-10).
|
||||||
pub fn decrypt_node_name_ext(dek: &[u8; 32], parent_id: i64, stored: &str, allow_legacy: bool) -> Option<String> {
|
pub fn decrypt_node_name_ext(
|
||||||
|
dek: &[u8; 32],
|
||||||
|
parent_id: i64,
|
||||||
|
stored: &str,
|
||||||
|
allow_legacy: bool,
|
||||||
|
) -> Option<String> {
|
||||||
// Abwärtskompatibilität für alte v0.2.0 $h$<nonce>$<tag>$<ct> Namen
|
// Abwärtskompatibilität für alte v0.2.0 $h$<nonce>$<tag>$<ct> Namen
|
||||||
if let Some(rest) = stored.strip_prefix("$h$") {
|
if let Some(rest) = stored.strip_prefix("$h$") {
|
||||||
let parts: Vec<&str> = rest.split('$').collect();
|
let parts: Vec<&str> = rest.split('$').collect();
|
||||||
@@ -423,7 +430,11 @@ pub fn levenshtein_distance(a: &str, b: &str) -> usize {
|
|||||||
|
|
||||||
for i in 1..=m {
|
for i in 1..=m {
|
||||||
for j in 1..=n {
|
for j in 1..=n {
|
||||||
let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
|
let cost = if a_chars[i - 1] == b_chars[j - 1] {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
dp[i][j] = (dp[i - 1][j] + 1)
|
dp[i][j] = (dp[i - 1][j] + 1)
|
||||||
.min(dp[i][j - 1] + 1)
|
.min(dp[i][j - 1] + 1)
|
||||||
.min(dp[i - 1][j - 1] + cost);
|
.min(dp[i - 1][j - 1] + cost);
|
||||||
@@ -471,10 +482,7 @@ pub fn normalize_mnemonic_phrase(phrase: &str) -> Vec<String> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
cleaned
|
cleaned.split_whitespace().map(|s| s.to_string()).collect()
|
||||||
.split_whitespace()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dekodiert eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase zurück in den 32-Byte DEK.
|
/// Dekodiert eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase zurück in den 32-Byte DEK.
|
||||||
@@ -500,9 +508,18 @@ pub fn mnemonic_to_dek(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
|
|||||||
if !word_list.contains(&word.as_str()) {
|
if !word_list.contains(&word.as_str()) {
|
||||||
let suggestion = suggest_bip39_word(word);
|
let suggestion = suggest_bip39_word(word);
|
||||||
let msg = if let Some(sug) = suggestion {
|
let msg = if let Some(sug) = suggestion {
|
||||||
format!("Wort #{} '{}' ist ungültig (Meinten Sie '{}'?)", idx + 1, word, sug)
|
format!(
|
||||||
|
"Wort #{} '{}' ist ungültig (Meinten Sie '{}'?)",
|
||||||
|
idx + 1,
|
||||||
|
word,
|
||||||
|
sug
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("Wort #{} '{}' ist ungültig (nicht im BIP-39 Wörterbuch)", idx + 1, word)
|
format!(
|
||||||
|
"Wort #{} '{}' ist ungültig (nicht im BIP-39 Wörterbuch)",
|
||||||
|
idx + 1,
|
||||||
|
word
|
||||||
|
)
|
||||||
};
|
};
|
||||||
invalid_words.push(msg);
|
invalid_words.push(msg);
|
||||||
}
|
}
|
||||||
@@ -620,7 +637,11 @@ pub fn decrypt_chunk(
|
|||||||
let mut buffer = ciphertext.to_vec();
|
let mut buffer = ciphertext.to_vec();
|
||||||
cipher
|
cipher
|
||||||
.decrypt_in_place_detached(nonce, &aad, &mut buffer, tag)
|
.decrypt_in_place_detached(nonce, &aad, &mut buffer, tag)
|
||||||
.map_err(|_| anyhow::anyhow!("Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"))?;
|
.map_err(|_| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Chunk-Integritätsprüfung fehlgeschlagen (AEAD Auth-Fehler oder Swap-Angriff)"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if format_version >= FORMAT_VERSION_V2 {
|
if format_version >= FORMAT_VERSION_V2 {
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
@@ -654,7 +675,6 @@ pub fn decrypt_chunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -700,26 +720,55 @@ mod tests {
|
|||||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2).unwrap();
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V2).unwrap();
|
||||||
|
|
||||||
// Reguläre Entschlüsselung (v2)
|
// Reguläre Entschlüsselung (v2)
|
||||||
let decrypted =
|
let decrypted = decrypt_chunk(
|
||||||
decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2).unwrap();
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ciphertext,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
FORMAT_VERSION_V2,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
|
|
||||||
// Swap Attack 1: Falsche node_id (Chunk in andere Datei verschoben)
|
// Swap Attack 1: Falsche node_id (Chunk in andere Datei verschoben)
|
||||||
let swap_node_err =
|
let swap_node_err = decrypt_chunk(
|
||||||
decrypt_chunk(&dek, 99i64, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2);
|
&dek,
|
||||||
|
99i64,
|
||||||
|
chunk_index,
|
||||||
|
&ciphertext,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
FORMAT_VERSION_V2,
|
||||||
|
);
|
||||||
assert!(swap_node_err.is_err());
|
assert!(swap_node_err.is_err());
|
||||||
|
|
||||||
// Swap Attack 2: Falscher chunk_index (Chunk innerhalb derselben Datei verschoben)
|
// Swap Attack 2: Falscher chunk_index (Chunk innerhalb derselben Datei verschoben)
|
||||||
let swap_idx_err =
|
let swap_idx_err = decrypt_chunk(
|
||||||
decrypt_chunk(&dek, node_id, 1u32, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2);
|
&dek,
|
||||||
|
node_id,
|
||||||
|
1u32,
|
||||||
|
&ciphertext,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
FORMAT_VERSION_V2,
|
||||||
|
);
|
||||||
assert!(swap_idx_err.is_err());
|
assert!(swap_idx_err.is_err());
|
||||||
|
|
||||||
// Manipulation des Ciphertexts
|
// Manipulation des Ciphertexts
|
||||||
let mut tampered_ct = ciphertext.clone();
|
let mut tampered_ct = ciphertext.clone();
|
||||||
tampered_ct[0] ^= 0x01;
|
tampered_ct[0] ^= 0x01;
|
||||||
assert!(
|
assert!(decrypt_chunk(
|
||||||
decrypt_chunk(&dek, node_id, chunk_index, &tampered_ct, &nonce, &tag, FORMAT_VERSION_V2).is_err()
|
&dek,
|
||||||
);
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&tampered_ct,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
FORMAT_VERSION_V2
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -742,8 +791,16 @@ mod tests {
|
|||||||
plaintext.len()
|
plaintext.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let decrypted =
|
let decrypted = decrypt_chunk(
|
||||||
decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V2).unwrap();
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ciphertext,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
FORMAT_VERSION_V2,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,8 +811,7 @@ mod tests {
|
|||||||
let mut random_bytes = vec![0u8; 1000];
|
let mut random_bytes = vec![0u8; 1000];
|
||||||
OsRng.fill_bytes(&mut random_bytes);
|
OsRng.fill_bytes(&mut random_bytes);
|
||||||
|
|
||||||
let (ct, nonce, tag) =
|
let (ct, nonce, tag) = encrypt_chunk(&dek, 1, 0, &random_bytes, FORMAT_VERSION_V2).unwrap();
|
||||||
encrypt_chunk(&dek, 1, 0, &random_bytes, FORMAT_VERSION_V2).unwrap();
|
|
||||||
// Da Kompression keine 64 Bytes spart, wird COMPRESSION_NONE (1 Byte) + Plaintext gespeichert
|
// Da Kompression keine 64 Bytes spart, wird COMPRESSION_NONE (1 Byte) + Plaintext gespeichert
|
||||||
assert_eq!(ct.len(), random_bytes.len() + 1);
|
assert_eq!(ct.len(), random_bytes.len() + 1);
|
||||||
|
|
||||||
@@ -775,8 +831,16 @@ mod tests {
|
|||||||
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V1).unwrap();
|
encrypt_chunk(&dek, node_id, chunk_index, plaintext, FORMAT_VERSION_V1).unwrap();
|
||||||
assert_eq!(ciphertext.len(), plaintext.len());
|
assert_eq!(ciphertext.len(), plaintext.len());
|
||||||
|
|
||||||
let decrypted =
|
let decrypted = decrypt_chunk(
|
||||||
decrypt_chunk(&dek, node_id, chunk_index, &ciphertext, &nonce, &tag, FORMAT_VERSION_V1).unwrap();
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ciphertext,
|
||||||
|
&nonce,
|
||||||
|
&tag,
|
||||||
|
FORMAT_VERSION_V1,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,10 +852,17 @@ mod tests {
|
|||||||
assert_eq!(words.len(), 24, "Mnemonic must have exactly 24 words");
|
assert_eq!(words.len(), 24, "Mnemonic must have exactly 24 words");
|
||||||
|
|
||||||
let recovered_dek = mnemonic_to_dek(&mnemonic_str).expect("Recover DEK");
|
let recovered_dek = mnemonic_to_dek(&mnemonic_str).expect("Recover DEK");
|
||||||
assert_eq!(*dek, *recovered_dek, "Recovered DEK must match original DEK");
|
assert_eq!(
|
||||||
|
*dek, *recovered_dek,
|
||||||
|
"Recovered DEK must match original DEK"
|
||||||
|
);
|
||||||
|
|
||||||
// Whitespace-Toleranz (z. B. doppelte Leerzeichen, Zeilenumbrüche)
|
// Whitespace-Toleranz (z. B. doppelte Leerzeichen, Zeilenumbrüche)
|
||||||
let messy_phrase = format!(" {} \n\t {} ", words[0..12].join(" "), words[12..24].join(" \n "));
|
let messy_phrase = format!(
|
||||||
|
" {} \n\t {} ",
|
||||||
|
words[0..12].join(" "),
|
||||||
|
words[12..24].join(" \n ")
|
||||||
|
);
|
||||||
let recovered_messy = mnemonic_to_dek(&messy_phrase).expect("Recover messy");
|
let recovered_messy = mnemonic_to_dek(&messy_phrase).expect("Recover messy");
|
||||||
assert_eq!(*dek, *recovered_messy);
|
assert_eq!(*dek, *recovered_messy);
|
||||||
}
|
}
|
||||||
@@ -815,9 +886,16 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
// Tausche das letzte Wort gegen ein anderes gültiges BIP-39 Wort
|
// Tausche das letzte Wort gegen ein anderes gültiges BIP-39 Wort
|
||||||
let original_last = words[23].clone();
|
let original_last = words[23].clone();
|
||||||
words[23] = if original_last == "abandon" { "zoo".to_string() } else { "abandon".to_string() };
|
words[23] = if original_last == "abandon" {
|
||||||
|
"zoo".to_string()
|
||||||
|
} else {
|
||||||
|
"abandon".to_string()
|
||||||
|
};
|
||||||
let corrupted_phrase = words.join(" ");
|
let corrupted_phrase = words.join(" ");
|
||||||
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]
|
#[test]
|
||||||
@@ -860,8 +938,14 @@ mod tests {
|
|||||||
assert_eq!(decrypted, filename);
|
assert_eq!(decrypted, filename);
|
||||||
|
|
||||||
// Abwärtskompatibilität: Legacy $h$<nonce>$<tag>$<ct> Format muss weiter entschlüsselt werden
|
// Abwärtskompatibilität: Legacy $h$<nonce>$<tag>$<ct> Format muss weiter entschlüsselt werden
|
||||||
let legacy_format = format!("$h${}${}${}", &encrypted[0..24], &encrypted[24..56], &encrypted[56..]);
|
let legacy_format = format!(
|
||||||
let decrypted_legacy = decrypt_node_name(&dek, parent_id, &legacy_format).expect("Decrypt legacy $h$ name");
|
"$h${}${}${}",
|
||||||
|
&encrypted[0..24],
|
||||||
|
&encrypted[24..56],
|
||||||
|
&encrypted[56..]
|
||||||
|
);
|
||||||
|
let decrypted_legacy =
|
||||||
|
decrypt_node_name(&dek, parent_id, &legacy_format).expect("Decrypt legacy $h$ name");
|
||||||
assert_eq!(decrypted_legacy, filename);
|
assert_eq!(decrypted_legacy, filename);
|
||||||
|
|
||||||
// Echte statische AAD Legacy-Verschlüsselung (b"SANCTUM_NODE_NAME")
|
// Echte statische AAD Legacy-Verschlüsselung (b"SANCTUM_NODE_NAME")
|
||||||
@@ -869,7 +953,11 @@ mod tests {
|
|||||||
let mut static_buf = filename.as_bytes().to_vec();
|
let mut static_buf = filename.as_bytes().to_vec();
|
||||||
let static_nonce = [42u8; 12];
|
let static_nonce = [42u8; 12];
|
||||||
let static_tag = cipher
|
let static_tag = cipher
|
||||||
.encrypt_in_place_detached(Nonce::from_slice(&static_nonce), b"SANCTUM_NODE_NAME", &mut static_buf)
|
.encrypt_in_place_detached(
|
||||||
|
Nonce::from_slice(&static_nonce),
|
||||||
|
b"SANCTUM_NODE_NAME",
|
||||||
|
&mut static_buf,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let legacy_static_format = format!(
|
let legacy_static_format = format!(
|
||||||
"$h${}${}${}",
|
"$h${}${}${}",
|
||||||
@@ -882,7 +970,8 @@ mod tests {
|
|||||||
|
|
||||||
// Mit aktiviertem Legacy-Flag darf es entschlüsselt werden
|
// Mit aktiviertem Legacy-Flag darf es entschlüsselt werden
|
||||||
set_allow_legacy_names(true);
|
set_allow_legacy_names(true);
|
||||||
let decrypted_static = decrypt_node_name(&dek, parent_id, &legacy_static_format).expect("Decrypt legacy static AAD name with flag");
|
let decrypted_static = decrypt_node_name(&dek, parent_id, &legacy_static_format)
|
||||||
|
.expect("Decrypt legacy static AAD name with flag");
|
||||||
assert_eq!(decrypted_static, filename);
|
assert_eq!(decrypted_static, filename);
|
||||||
set_allow_legacy_names(false);
|
set_allow_legacy_names(false);
|
||||||
|
|
||||||
@@ -904,17 +993,33 @@ mod tests {
|
|||||||
let enc_folder_a = encrypt_node_name(&dek, 10, "secrets.txt");
|
let enc_folder_a = encrypt_node_name(&dek, 10, "secrets.txt");
|
||||||
let enc_folder_b = encrypt_node_name(&dek, 20, "passwords.txt");
|
let enc_folder_b = encrypt_node_name(&dek, 20, "passwords.txt");
|
||||||
// Gültige parent_ids entschlüsseln erfolgreich
|
// Gültige parent_ids entschlüsseln erfolgreich
|
||||||
assert_eq!(decrypt_node_name(&dek, 10, &enc_folder_a).unwrap(), "secrets.txt");
|
assert_eq!(
|
||||||
assert_eq!(decrypt_node_name(&dek, 20, &enc_folder_b).unwrap(), "passwords.txt");
|
decrypt_node_name(&dek, 10, &enc_folder_a).unwrap(),
|
||||||
|
"secrets.txt"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
decrypt_node_name(&dek, 20, &enc_folder_b).unwrap(),
|
||||||
|
"passwords.txt"
|
||||||
|
);
|
||||||
|
|
||||||
// Swap-Angriff: Ein Angreifer verschiebt enc_folder_a in Ordner 20
|
// Swap-Angriff: Ein Angreifer verschiebt enc_folder_a in Ordner 20
|
||||||
assert!(decrypt_node_name(&dek, 20, &enc_folder_a).is_none(), "Swap in anderen Ordner muss durch AAD fehlschlagen!");
|
assert!(
|
||||||
assert!(decrypt_node_name(&dek, 10, &enc_folder_b).is_none(), "Swap in anderen Ordner muss durch AAD fehlschlagen!");
|
decrypt_node_name(&dek, 20, &enc_folder_a).is_none(),
|
||||||
|
"Swap in anderen Ordner muss durch AAD fehlschlagen!"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
decrypt_node_name(&dek, 10, &enc_folder_b).is_none(),
|
||||||
|
"Swap in anderen Ordner muss durch AAD fehlschlagen!"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_model_a_slot_payloads() {
|
fn test_model_a_slot_payloads() {
|
||||||
let test_kdf = KdfParams { memory_cost: MIN_MEMORY_COST_KIB, time_cost: MIN_TIME_COST, parallelism: 1 };
|
let test_kdf = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &test_kdf).unwrap();
|
let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &test_kdf).unwrap();
|
||||||
let kek_1 = derive_kek("HiddenPass123!", &generate_salt(), &test_kdf).unwrap();
|
let kek_1 = derive_kek("HiddenPass123!", &generate_salt(), &test_kdf).unwrap();
|
||||||
let dek_0 = generate_dek();
|
let dek_0 = generate_dek();
|
||||||
@@ -922,7 +1027,8 @@ mod tests {
|
|||||||
let carrier_node_id = 42i64;
|
let carrier_node_id = 42i64;
|
||||||
|
|
||||||
// Slot 0 Payload: 40 Bytes
|
// Slot 0 Payload: 40 Bytes
|
||||||
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
let (wrapped_0, nonce_0, tag_0) =
|
||||||
|
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
assert_eq!(wrapped_0.len(), 40);
|
assert_eq!(wrapped_0.len(), 40);
|
||||||
|
|
||||||
let unwrapped_0 = unwrap_key_payload(&kek_0, &wrapped_0, &nonce_0, &tag_0).unwrap();
|
let unwrapped_0 = unwrap_key_payload(&kek_0, &wrapped_0, &nonce_0, &tag_0).unwrap();
|
||||||
@@ -932,7 +1038,8 @@ mod tests {
|
|||||||
assert_eq!(recovered_cid_0, carrier_node_id);
|
assert_eq!(recovered_cid_0, carrier_node_id);
|
||||||
|
|
||||||
// Slot 1 Payload: 72 Bytes
|
// Slot 1 Payload: 72 Bytes
|
||||||
let (wrapped_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
assert_eq!(wrapped_1.len(), 72);
|
assert_eq!(wrapped_1.len(), 72);
|
||||||
|
|
||||||
let unwrapped_1 = unwrap_key_payload(&kek_1, &wrapped_1, &nonce_1, &tag_1).unwrap();
|
let unwrapped_1 = unwrap_key_payload(&kek_1, &wrapped_1, &nonce_1, &tag_1).unwrap();
|
||||||
@@ -964,15 +1071,30 @@ mod tests {
|
|||||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||||
|
|
||||||
let mut ct = malicious_plaintext.clone();
|
let mut ct = malicious_plaintext.clone();
|
||||||
let tag = cipher.encrypt_in_place_detached(nonce, &aad, &mut ct).unwrap();
|
let tag = cipher
|
||||||
|
.encrypt_in_place_detached(nonce, &aad, &mut ct)
|
||||||
|
.unwrap();
|
||||||
let tag_bytes: [u8; 16] = tag.as_slice().try_into().unwrap();
|
let tag_bytes: [u8; 16] = tag.as_slice().try_into().unwrap();
|
||||||
|
|
||||||
// Entschlüsselung muss fehlschlagen, da Dekomprimierungs-Bomb-Schutz greift
|
// Entschlüsselung muss fehlschlagen, da Dekomprimierungs-Bomb-Schutz greift
|
||||||
let res = decrypt_chunk(&dek, node_id, chunk_index, &ct, &nonce_bytes, &tag_bytes, FORMAT_VERSION_V2);
|
let res = decrypt_chunk(
|
||||||
assert!(res.is_err(), "Dekomprimierungs-Bomb über 1 MB muss abgewiesen werden!");
|
&dek,
|
||||||
|
node_id,
|
||||||
|
chunk_index,
|
||||||
|
&ct,
|
||||||
|
&nonce_bytes,
|
||||||
|
&tag_bytes,
|
||||||
|
FORMAT_VERSION_V2,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"Dekomprimierungs-Bomb über 1 MB muss abgewiesen werden!"
|
||||||
|
);
|
||||||
let err_msg = res.err().unwrap().to_string();
|
let err_msg = res.err().unwrap().to_string();
|
||||||
assert!(err_msg.contains("Decompression-Bomb Schutz ausgelöst"), "Fehlermeldung erwartet: {}", err_msg);
|
assert!(
|
||||||
|
err_msg.contains("Decompression-Bomb Schutz ausgelöst"),
|
||||||
|
"Fehlermeldung erwartet: {}",
|
||||||
|
err_msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+361
-110
@@ -295,18 +295,30 @@ fn parse_drive_letter(s: &str) -> Result<char> {
|
|||||||
pub fn parse_size_string(s: &str) -> Result<u64> {
|
pub fn parse_size_string(s: &str) -> Result<u64> {
|
||||||
let trimmed = s.trim().to_uppercase();
|
let trimmed = s.trim().to_uppercase();
|
||||||
if let Some(num_str) = trimmed.strip_suffix("GB") {
|
if let Some(num_str) = trimmed.strip_suffix("GB") {
|
||||||
let n: u64 = num_str.trim().parse().context("Ungültige Gigabyte-Angabe")?;
|
let n: u64 = num_str
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.context("Ungültige Gigabyte-Angabe")?;
|
||||||
Ok(n * 1024 * 1024 * 1024)
|
Ok(n * 1024 * 1024 * 1024)
|
||||||
} else if let Some(num_str) = trimmed.strip_suffix("MB") {
|
} else if let Some(num_str) = trimmed.strip_suffix("MB") {
|
||||||
let n: u64 = num_str.trim().parse().context("Ungültige Megabyte-Angabe")?;
|
let n: u64 = num_str
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.context("Ungültige Megabyte-Angabe")?;
|
||||||
Ok(n * 1024 * 1024)
|
Ok(n * 1024 * 1024)
|
||||||
} else if let Some(num_str) = trimmed.strip_suffix("KB") {
|
} else if let Some(num_str) = trimmed.strip_suffix("KB") {
|
||||||
let n: u64 = num_str.trim().parse().context("Ungültige Kilobyte-Angabe")?;
|
let n: u64 = num_str
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.context("Ungültige Kilobyte-Angabe")?;
|
||||||
Ok(n * 1024)
|
Ok(n * 1024)
|
||||||
} else if let Ok(n) = trimmed.parse::<u64>() {
|
} else if let Ok(n) = trimmed.parse::<u64>() {
|
||||||
Ok(n)
|
Ok(n)
|
||||||
} else {
|
} else {
|
||||||
bail!("Ungültiges Größenformat: '{}'. Erwartet z. B. '500MB', '1GB', '2GB'", s);
|
bail!(
|
||||||
|
"Ungültiges Größenformat: '{}'. Erwartet z. B. '500MB', '1GB', '2GB'",
|
||||||
|
s
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,9 +331,14 @@ pub fn parse_size_string(s: &str) -> Result<u64> {
|
|||||||
fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>> {
|
fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>> {
|
||||||
if let Some(key) = arg {
|
if let Some(key) = arg {
|
||||||
if key == "-" {
|
if key == "-" {
|
||||||
println!(" [{}] Lese 24-Wort Notfallschlüssel von der Standardeingabe (stdin)...", ui::cyan("ℹ"));
|
println!(
|
||||||
|
" [{}] Lese 24-Wort Notfallschlüssel von der Standardeingabe (stdin)...",
|
||||||
|
ui::cyan("ℹ")
|
||||||
|
);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
std::io::stdin().read_line(&mut line).context("Fehler beim Einlesen von stdin")?;
|
std::io::stdin()
|
||||||
|
.read_line(&mut line)
|
||||||
|
.context("Fehler beim Einlesen von stdin")?;
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
bail!("Der Notfallschlüssel von stdin darf nicht leer sein.");
|
bail!("Der Notfallschlüssel von stdin darf nicht leer sein.");
|
||||||
@@ -342,7 +359,10 @@ fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>>
|
|||||||
std::env::remove_var("SANCTUM_RECOVERY_KEY");
|
std::env::remove_var("SANCTUM_RECOVERY_KEY");
|
||||||
let trimmed = env_key.trim();
|
let trimmed = env_key.trim();
|
||||||
if !trimmed.is_empty() {
|
if !trimmed.is_empty() {
|
||||||
println!(" [{}] Verwende Notfallschlüssel aus Umgebungsvariable 'SANCTUM_RECOVERY_KEY'.", ui::cyan("ℹ"));
|
println!(
|
||||||
|
" [{}] Verwende Notfallschlüssel aus Umgebungsvariable 'SANCTUM_RECOVERY_KEY'.",
|
||||||
|
ui::cyan("ℹ")
|
||||||
|
);
|
||||||
Ok(Some(Zeroizing::new(trimmed.to_string())))
|
Ok(Some(Zeroizing::new(trimmed.to_string())))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -355,8 +375,14 @@ fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>>
|
|||||||
/// Liest den 24-Wort BIP-39 Notfallschlüssel interaktiv und maskiert ein,
|
/// Liest den 24-Wort BIP-39 Notfallschlüssel interaktiv und maskiert ein,
|
||||||
/// ohne dass Eingaben in der PowerShell-Historie (ConsoleHost_history.txt) landen.
|
/// ohne dass Eingaben in der PowerShell-Historie (ConsoleHost_history.txt) landen.
|
||||||
fn prompt_recovery_key_interactive() -> Result<Zeroizing<String>> {
|
fn prompt_recovery_key_interactive() -> Result<Zeroizing<String>> {
|
||||||
println!(" [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.", ui::cyan("ℹ"));
|
println!(
|
||||||
println!(" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der Shell-Historie.", ui::dim("OpSec"));
|
" [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.",
|
||||||
|
ui::cyan("ℹ")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der Shell-Historie.",
|
||||||
|
ui::dim("OpSec")
|
||||||
|
);
|
||||||
let phrase = rpassword::prompt_password("24-Wort Notfallschlüssel: ")
|
let phrase = rpassword::prompt_password("24-Wort Notfallschlüssel: ")
|
||||||
.context("Fehler beim Einlesen des Notfallschlüssels")?;
|
.context("Fehler beim Einlesen des Notfallschlüssels")?;
|
||||||
if phrase.trim().is_empty() {
|
if phrase.trim().is_empty() {
|
||||||
@@ -383,8 +409,15 @@ fn handle_init(
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" Zieldatei: {}", container_path.display());
|
println!(" Zieldatei: {}", container_path.display());
|
||||||
if with_hidden {
|
if with_hidden {
|
||||||
println!(" Modus: Dual-Vault (Modell A: {})", ui::magenta("Carrier-Datei"));
|
println!(
|
||||||
println!(" Carrier: {} ({})", ui::cyan(carrier_name), ui::cyan(carrier_size_str));
|
" Modus: Dual-Vault (Modell A: {})",
|
||||||
|
ui::magenta("Carrier-Datei")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Carrier: {} ({})",
|
||||||
|
ui::cyan(carrier_name),
|
||||||
|
ui::cyan(carrier_size_str)
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
" [{}] Hinweis zum Dual-Vault: Modell A kapselt den Hidden Vault in einer Datei im Decoy-Vault. Dies schützt vor neugierigen Blicken, widersteht jedoch keinem forensischen Gutachten, da Carrier-Entropie und Schreibmuster analysierbar sind.",
|
" [{}] Hinweis zum Dual-Vault: Modell A kapselt den Hidden Vault in einer Datei im Decoy-Vault. Dies schützt vor neugierigen Blicken, widersteht jedoch keinem forensischen Gutachten, da Carrier-Entropie und Schreibmuster analysierbar sind.",
|
||||||
ui::yellow("Info")
|
ui::yellow("Info")
|
||||||
@@ -415,7 +448,10 @@ fn handle_init(
|
|||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!(" ─── [2/2] Hidden Vault (Zweiter isolierter Tresor / Dual-Vault) ───");
|
println!(" ─── [2/2] Hidden Vault (Zweiter isolierter Tresor / Dual-Vault) ───");
|
||||||
println!(" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG"));
|
println!(
|
||||||
|
" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!",
|
||||||
|
ui::yellow("WICHTIG")
|
||||||
|
);
|
||||||
let password_1 = Zeroizing::new(
|
let password_1 = Zeroizing::new(
|
||||||
rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
||||||
.context("Fehler beim Einlesen des Passworts")?,
|
.context("Fehler beim Einlesen des Passworts")?,
|
||||||
@@ -432,7 +468,12 @@ fn handle_init(
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
ui::step(1, 4, "🔑", "Leite KEKs für Standard- und Hidden-Vault via Argon2id ab...");
|
ui::step(
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
"🔑",
|
||||||
|
"Leite KEKs für Standard- und Hidden-Vault via Argon2id ab...",
|
||||||
|
);
|
||||||
let salt_0 = generate_salt();
|
let salt_0 = generate_salt();
|
||||||
let kdf_params_0 = KdfParams::default();
|
let kdf_params_0 = KdfParams::default();
|
||||||
let kek_0 = derive_kek(&password_0, &salt_0, &kdf_params_0)?;
|
let kek_0 = derive_kek(&password_0, &salt_0, &kdf_params_0)?;
|
||||||
@@ -445,14 +486,24 @@ fn handle_init(
|
|||||||
let dek_0 = generate_dek();
|
let dek_0 = generate_dek();
|
||||||
let dek_1 = generate_dek();
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
ui::step(3, 4, "🔒", "Verschlüssele Slot-Payloads (Dual-Vault Key-Wrapping)...");
|
ui::step(
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
"🔒",
|
||||||
|
"Verschlüssele Slot-Payloads (Dual-Vault Key-Wrapping)...",
|
||||||
|
);
|
||||||
let carrier_node_id = 3i64;
|
let carrier_node_id = 3i64;
|
||||||
let (wrapped_dek_0, header_nonce_0, header_tag_0) =
|
let (wrapped_dek_0, header_nonce_0, header_tag_0) =
|
||||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?;
|
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?;
|
||||||
|
|
||||||
ui::step(4, 4, "📦", "Erzeuge SQLite-Container & alloziiere Alibi-Carrier-Datei...");
|
ui::step(
|
||||||
let db = Database::open(container_path)
|
4,
|
||||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
4,
|
||||||
|
"📦",
|
||||||
|
"Erzeuge SQLite-Container & alloziiere Alibi-Carrier-Datei...",
|
||||||
|
);
|
||||||
|
let db =
|
||||||
|
Database::open(container_path).context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||||
|
|
||||||
let (wrapped_dek_1, header_nonce_1, header_tag_1) =
|
let (wrapped_dek_1, header_nonce_1, header_tag_1) =
|
||||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id)?;
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id)?;
|
||||||
@@ -487,7 +538,10 @@ fn handle_init(
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
println!(
|
||||||
|
" • Format: Version {} (Magic: SANCTUM\\0)",
|
||||||
|
FORMAT_VERSION
|
||||||
|
);
|
||||||
println!(" • Carrier-Datei:{} ({})", carrier_name, carrier_size_str);
|
println!(" • Carrier-Datei:{} ({})", carrier_name, carrier_size_str);
|
||||||
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
||||||
println!(" • Kapselung: Hidden Vault liegt in Carrier-Datei im Decoy-Vault");
|
println!(" • Kapselung: Hidden Vault liegt in Carrier-Datei im Decoy-Vault");
|
||||||
@@ -495,8 +549,16 @@ fn handle_init(
|
|||||||
println!(" • Dateigröße: Feste Trägergröße zur Vermeidung von Größenveränderungen");
|
println!(" • Dateigröße: Feste Trägergröße zur Vermeidung von Größenveränderungen");
|
||||||
println!();
|
println!();
|
||||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
println!(
|
||||||
println!(" (Die Eingabe des jeweiligen Passworts bindet automatisch den passenden Vault ein)");
|
" {}",
|
||||||
|
ui::cyan(&format!(
|
||||||
|
"sanctum mount --path \"{}\"",
|
||||||
|
container_path.display()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" (Die Eingabe des jeweiligen Passworts bindet automatisch den passenden Vault ein)"
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
println!(" ┌─────────────────────────────────────────────────────────┐");
|
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||||||
@@ -525,25 +587,47 @@ fn handle_init(
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
ui::step(
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
"🔑",
|
||||||
|
"Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...",
|
||||||
|
);
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
let kdf_params = KdfParams::default();
|
let kdf_params = KdfParams::default();
|
||||||
let kek = derive_kek(&password, &salt, &kdf_params)
|
let kek = derive_kek(&password, &salt, &kdf_params)
|
||||||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||||||
|
|
||||||
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
ui::step(
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
"🎲",
|
||||||
|
"Erzeuge kryptografisch sicheren DEK via CSPRNG...",
|
||||||
|
);
|
||||||
let dek = generate_dek();
|
let dek = generate_dek();
|
||||||
|
|
||||||
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
||||||
let (wrapped_dek, header_nonce, header_tag) =
|
let (wrapped_dek, header_nonce, header_tag) =
|
||||||
wrap_slot0_payload(&kek, &dek, 0).context("DEK-Wrapping fehlgeschlagen")?;
|
wrap_slot0_payload(&kek, &dek, 0).context("DEK-Wrapping fehlgeschlagen")?;
|
||||||
|
|
||||||
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
ui::step(
|
||||||
let db = Database::open(container_path)
|
4,
|
||||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
4,
|
||||||
|
"📦",
|
||||||
|
"Initialisiere SQLite-Containerstruktur & WAL-Modus...",
|
||||||
|
);
|
||||||
|
let db =
|
||||||
|
Database::open(container_path).context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||||
|
|
||||||
db.init_schema_with_carrier(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag, None)
|
db.init_schema_with_carrier(
|
||||||
.context("Fehler bei der Schema-Initialisierung")?;
|
&salt,
|
||||||
|
&kdf_params,
|
||||||
|
&wrapped_dek,
|
||||||
|
&header_nonce,
|
||||||
|
&header_tag,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("Fehler bei der Schema-Initialisierung")?;
|
||||||
|
|
||||||
db.checkpoint()
|
db.checkpoint()
|
||||||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||||||
@@ -557,13 +641,22 @@ fn handle_init(
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
println!(
|
||||||
|
" • Format: Version {} (Magic: SANCTUM\\0)",
|
||||||
|
FORMAT_VERSION
|
||||||
|
);
|
||||||
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||||
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
||||||
println!(" • Dual-Vault: Slot 1 mit Zufallsrauschen initialisiert (inaktiv)");
|
println!(" • Dual-Vault: Slot 1 mit Zufallsrauschen initialisiert (inaktiv)");
|
||||||
println!();
|
println!();
|
||||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
println!(
|
||||||
|
" {}",
|
||||||
|
ui::cyan(&format!(
|
||||||
|
"sanctum mount --path \"{}\"",
|
||||||
|
container_path.display()
|
||||||
|
))
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
ui::print_recovery_phrase_card(&recovery_phrase);
|
ui::print_recovery_phrase_card(&recovery_phrase);
|
||||||
@@ -574,7 +667,10 @@ fn handle_init(
|
|||||||
|
|
||||||
fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
@@ -583,13 +679,16 @@ fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
|||||||
println!(" Container: {}", container_path.display());
|
println!(" Container: {}", container_path.display());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
|
||||||
|
|
||||||
let freelist_before = db.freelist_count()
|
let freelist_before = db
|
||||||
|
.freelist_count()
|
||||||
.context("Konnte Freelist-Größe nicht ermitteln")?;
|
.context("Konnte Freelist-Größe nicht ermitteln")?;
|
||||||
|
|
||||||
println!(" • Freie Seiten in Freelist: {}", ui::cyan(&freelist_before.to_string()));
|
println!(
|
||||||
|
" • Freie Seiten in Freelist: {}",
|
||||||
|
ui::cyan(&freelist_before.to_string())
|
||||||
|
);
|
||||||
|
|
||||||
if freelist_before == 0 {
|
if freelist_before == 0 {
|
||||||
println!(" • Der Container ist bereits optimal kompaktiert. Keine Bereinigung nötig.");
|
println!(" • Der Container ist bereits optimal kompaktiert. Keine Bereinigung nötig.");
|
||||||
@@ -598,7 +697,8 @@ fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui::step(1, 2, "🧹", "Führe Incremental-Vacuum aus...");
|
ui::step(1, 2, "🧹", "Führe Incremental-Vacuum aus...");
|
||||||
let freed = db.incremental_vacuum(pages)
|
let freed = db
|
||||||
|
.incremental_vacuum(pages)
|
||||||
.context("Fehler bei der Speicherbereinigung (Incremental Vacuum)")?;
|
.context("Fehler bei der Speicherbereinigung (Incremental Vacuum)")?;
|
||||||
|
|
||||||
ui::step(2, 2, "💾", "Führe finalen WAL-Checkpoint durch...");
|
ui::step(2, 2, "💾", "Führe finalen WAL-Checkpoint durch...");
|
||||||
@@ -612,8 +712,14 @@ fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
|||||||
println!("│ ✔ Container erfolgreich kompaktiert! │");
|
println!("│ ✔ Container erfolgreich kompaktiert! │");
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Freigegebene Seiten: {}", ui::green(&freed.to_string()));
|
println!(
|
||||||
println!(" • Verbleibende Freeseiten: {}", ui::dim(&freelist_after.to_string()));
|
" • Freigegebene Seiten: {}",
|
||||||
|
ui::green(&freed.to_string())
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" • Verbleibende Freeseiten: {}",
|
||||||
|
ui::dim(&freelist_after.to_string())
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -632,24 +738,29 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
println!(" Container: {}", container_path.display());
|
println!(" Container: {}", container_path.display());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).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(actual_phrase) = resolve_recovery_key(recovery_key)? {
|
let (dek, target_slot_id, carrier_dek, carrier_node_id) = if let Some(actual_phrase) =
|
||||||
|
resolve_recovery_key(recovery_key)?
|
||||||
|
{
|
||||||
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(&actual_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!");
|
||||||
|
|
||||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
let meta = db
|
||||||
|
.read_meta()
|
||||||
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
let detected_slot = detect_recovery_key_slot(&db, &d, meta.version).unwrap_or(0);
|
let detected_slot = detect_recovery_key_slot(&db, &d, meta.version).unwrap_or(0);
|
||||||
if detected_slot == 1 {
|
if detected_slot == 1 {
|
||||||
println!(" Notfallschlüssel gehört zu Slot 1 (Hidden Vault).");
|
println!(" Notfallschlüssel gehört zu Slot 1 (Hidden Vault).");
|
||||||
println!(" Zur Neuverpackung von Slot 1 wird das Passwort des Decoy-Tresors (Slot 0) benötigt.");
|
println!(" Zur Neuverpackung von Slot 1 wird das Passwort des Decoy-Tresors (Slot 0) benötigt.");
|
||||||
let decoy_pw = Zeroizing::new(
|
let decoy_pw = Zeroizing::new(
|
||||||
rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ")
|
rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ")
|
||||||
.context("Fehler beim Einlesen des Decoy-Passworts")?
|
.context("Fehler beim Einlesen des Decoy-Passworts")?,
|
||||||
);
|
);
|
||||||
let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen."))?;
|
let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen.")
|
||||||
|
})?;
|
||||||
let c_dek = decoy_auth.dek().clone();
|
let c_dek = decoy_auth.dek().clone();
|
||||||
(d, 1, Some(c_dek), db.find_carrier_node_id()?)
|
(d, 1, Some(c_dek), db.find_carrier_node_id()?)
|
||||||
} else {
|
} else {
|
||||||
@@ -671,10 +782,17 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
.read_meta()
|
.read_meta()
|
||||||
.context("Konnte Container-Header nicht lesen")?;
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab & prüfe Passwort (konstante Zeit)...");
|
ui::step(
|
||||||
let keys = meta
|
2,
|
||||||
.authenticate(&old_password)
|
4,
|
||||||
.ok_or_else(|| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?;
|
"🔑",
|
||||||
|
"Leite KEK via Argon2id ab & prüfe Passwort (konstante Zeit)...",
|
||||||
|
);
|
||||||
|
let keys = meta.authenticate(&old_password).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let d = keys.dek().clone();
|
let d = keys.dek().clone();
|
||||||
let slot_id = keys.slot_id();
|
let slot_id = keys.slot_id();
|
||||||
(d, slot_id, keys.carrier_dek(), keys.carrier_node_id())
|
(d, slot_id, keys.carrier_dek(), keys.carrier_node_id())
|
||||||
@@ -697,14 +815,20 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
ui::step(3, 4, "🔒", "Generiere frisches Salt & leite neuen KEK ab...");
|
ui::step(
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
"🔒",
|
||||||
|
"Generiere frisches Salt & leite neuen KEK ab...",
|
||||||
|
);
|
||||||
let new_salt = generate_salt();
|
let new_salt = generate_salt();
|
||||||
let new_params = KdfParams::default();
|
let new_params = KdfParams::default();
|
||||||
let new_kek = derive_kek(&new_password, &new_salt, &new_params)
|
let new_kek = derive_kek(&new_password, &new_salt, &new_params)
|
||||||
.context("KDF-Schlüsselableitung mit neuem Passwort fehlgeschlagen")?;
|
.context("KDF-Schlüsselableitung mit neuem Passwort fehlgeschlagen")?;
|
||||||
|
|
||||||
let (new_wrapped_dek, new_nonce, new_tag) = if target_slot_id == 1 {
|
let (new_wrapped_dek, new_nonce, new_tag) = if target_slot_id == 1 {
|
||||||
let c_dek = carrier_dek.ok_or_else(|| anyhow::anyhow!("DEK_0 (Träger-Schlüssel) für Slot 1 fehlt"))?;
|
let c_dek = carrier_dek
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("DEK_0 (Träger-Schlüssel) für Slot 1 fehlt"))?;
|
||||||
let c_nid = carrier_node_id.unwrap_or(0);
|
let c_nid = carrier_node_id.unwrap_or(0);
|
||||||
wrap_slot1_payload(&new_kek, &dek, &c_dek, c_nid)
|
wrap_slot1_payload(&new_kek, &dek, &c_dek, c_nid)
|
||||||
.context("Slot-1 Wrapping mit neuem KEK fehlgeschlagen")?
|
.context("Slot-1 Wrapping mit neuem KEK fehlgeschlagen")?
|
||||||
@@ -714,7 +838,12 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
.context("Slot-0 Wrapping mit neuem KEK fehlgeschlagen")?
|
.context("Slot-0 Wrapping mit neuem KEK fehlgeschlagen")?
|
||||||
};
|
};
|
||||||
|
|
||||||
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
|
ui::step(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
"💾",
|
||||||
|
"Aktualisiere Container-Header & führe Checkpoint aus...",
|
||||||
|
);
|
||||||
db.update_slot_keys(
|
db.update_slot_keys(
|
||||||
target_slot_id,
|
target_slot_id,
|
||||||
&new_salt,
|
&new_salt,
|
||||||
@@ -774,10 +903,16 @@ fn handle_backup_header(container_path: &Path, output_path: Option<&Path>) -> Re
|
|||||||
|
|
||||||
fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
|
fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if output_path.exists() {
|
if output_path.exists() {
|
||||||
bail!("Zieldatei '{}' existiert bereits. Bitte wählen Sie einen anderen Pfad.", output_path.display());
|
bail!(
|
||||||
|
"Zieldatei '{}' existiert bereits. Bitte wählen Sie einen anderen Pfad.",
|
||||||
|
output_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
@@ -787,11 +922,20 @@ fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
|
|||||||
println!(" Backup-Ziel: {}", output_path.display());
|
println!(" Backup-Ziel: {}", output_path.display());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
ui::step(1, 2, "📦", "Öffne Container & initialisiere Online-Backup-Stream...");
|
ui::step(
|
||||||
let db = Database::open(container_path)
|
1,
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
2,
|
||||||
|
"📦",
|
||||||
|
"Öffne Container & initialisiere Online-Backup-Stream...",
|
||||||
|
);
|
||||||
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
ui::step(2, 2, "💾", "Kopiere Datenbankseiten konsistent via SQLite Backup API...");
|
ui::step(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
"💾",
|
||||||
|
"Kopiere Datenbankseiten konsistent via SQLite Backup API...",
|
||||||
|
);
|
||||||
db.online_backup(output_path)
|
db.online_backup(output_path)
|
||||||
.context("Fehler beim Erstellen des Online-Backups")?;
|
.context("Fehler beim Erstellen des Online-Backups")?;
|
||||||
|
|
||||||
@@ -801,7 +945,9 @@ fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Backup-Datei: {}", output_path.display());
|
println!(" • Backup-Datei: {}", output_path.display());
|
||||||
println!(" • Status: Konsistent & verifiziert (kann im laufenden Betrieb gesichert werden)");
|
println!(
|
||||||
|
" • Status: Konsistent & verifiziert (kann im laufenden Betrieb gesichert werden)"
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -826,7 +972,12 @@ fn handle_restore(backup_path: &Path, output_path: &Path) -> Result<()> {
|
|||||||
Database::restore_from_backup(backup_path, output_path)
|
Database::restore_from_backup(backup_path, output_path)
|
||||||
.context("Fehler bei der Container-Wiederherstellung aus dem Backup")?;
|
.context("Fehler bei der Container-Wiederherstellung aus dem Backup")?;
|
||||||
|
|
||||||
ui::step(2, 2, "🔍", "B-Tree Integritätsprüfung (PRAGMA quick_check) erfolgreich!");
|
ui::step(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
"🔍",
|
||||||
|
"B-Tree Integritätsprüfung (PRAGMA quick_check) erfolgreich!",
|
||||||
|
);
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
@@ -856,9 +1007,17 @@ fn handle_restore_header(
|
|||||||
println!(" Verwende Backup-Datei: {}", hdr.display());
|
println!(" Verwende Backup-Datei: {}", hdr.display());
|
||||||
ui::step(1, 2, "📦", "Lese und validiere Header-Backup...");
|
ui::step(1, 2, "📦", "Lese und validiere Header-Backup...");
|
||||||
restore_header_backup(container_path, hdr)?;
|
restore_header_backup(container_path, hdr)?;
|
||||||
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 {
|
} else {
|
||||||
let key_str = if let Some(key) = resolve_recovery_key(recovery_key)? {
|
let key_str = if let Some(key) = resolve_recovery_key(recovery_key)? {
|
||||||
key
|
key
|
||||||
@@ -887,7 +1046,8 @@ fn handle_restore_header(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let dek = mnemonic_to_dek(&key_str).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
let dek = mnemonic_to_dek(&key_str).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
let db =
|
||||||
|
Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
let effective_slot = if slot == 0 {
|
let effective_slot = if slot == 0 {
|
||||||
detect_recovery_key_slot(&db, &dek, FORMAT_VERSION).unwrap_or(0)
|
detect_recovery_key_slot(&db, &dek, FORMAT_VERSION).unwrap_or(0)
|
||||||
@@ -895,7 +1055,10 @@ fn handle_restore_header(
|
|||||||
slot
|
slot
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", effective_slot);
|
println!(
|
||||||
|
" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...",
|
||||||
|
effective_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: ")
|
||||||
.context("Fehler beim Einlesen des Passworts")?,
|
.context("Fehler beim Einlesen des Passworts")?,
|
||||||
@@ -916,10 +1079,14 @@ fn handle_restore_header(
|
|||||||
println!(" Zur mathematischen Rekonstruktion wird das Passwort des Decoy-Tresors (Slot 0) benötigt.");
|
println!(" Zur mathematischen Rekonstruktion wird das Passwort des Decoy-Tresors (Slot 0) benötigt.");
|
||||||
let decoy_pw = Zeroizing::new(
|
let decoy_pw = Zeroizing::new(
|
||||||
rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ")
|
rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ")
|
||||||
.context("Fehler beim Einlesen des Decoy-Passworts")?
|
.context("Fehler beim Einlesen des Decoy-Passworts")?,
|
||||||
);
|
);
|
||||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
let meta = db
|
||||||
let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen."))?;
|
.read_meta()
|
||||||
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen.")
|
||||||
|
})?;
|
||||||
Some(**decoy_auth.dek())
|
Some(**decoy_auth.dek())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -927,10 +1094,27 @@ fn handle_restore_header(
|
|||||||
let dek_0_opt = dek_0_holder.as_ref();
|
let dek_0_opt = dek_0_holder.as_ref();
|
||||||
|
|
||||||
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_str, &new_password, effective_slot, dek_0_opt)?;
|
restore_slot_from_recovery_key(
|
||||||
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", effective_slot));
|
container_path,
|
||||||
|
&key_str,
|
||||||
|
&new_password,
|
||||||
|
effective_slot,
|
||||||
|
dek_0_opt,
|
||||||
|
)?;
|
||||||
|
ui::step(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
"💾",
|
||||||
|
&format!(
|
||||||
|
"Header für Slot {} mit neuem Passwort neu synthetisiert!",
|
||||||
|
effective_slot
|
||||||
|
),
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("{}", ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!"));
|
println!(
|
||||||
|
"{}",
|
||||||
|
ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@@ -939,7 +1123,10 @@ fn handle_restore_header(
|
|||||||
|
|
||||||
fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
@@ -953,9 +1140,10 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
|||||||
.context("Fehler beim Einlesen des Passworts")?,
|
.context("Fehler beim Einlesen des Passworts")?,
|
||||||
);
|
);
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
let meta = db
|
||||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
.read_meta()
|
||||||
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
let keys = meta
|
let keys = meta
|
||||||
.authenticate(&password)
|
.authenticate(&password)
|
||||||
@@ -970,7 +1158,10 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
|||||||
|
|
||||||
fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
@@ -994,12 +1185,18 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
|||||||
Some(keys.dek().clone())
|
Some(keys.dek().clone())
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
println!(" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.", ui::yellow("⚠️"));
|
println!(
|
||||||
|
" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.",
|
||||||
|
ui::yellow("⚠️")
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(" {} Kein Passwort angegeben. Führe nur Strukturprüfung durch.", ui::dim("ℹ"));
|
println!(
|
||||||
|
" {} Kein Passwort angegeben. Führe nur Strukturprüfung durch.",
|
||||||
|
ui::dim("ℹ")
|
||||||
|
);
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1026,7 +1223,10 @@ fn handle_sync(
|
|||||||
recovery_key: Option<&str>,
|
recovery_key: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth = if let Some(phrase_str) = resolve_recovery_key(recovery_key)? {
|
let auth = if let Some(phrase_str) = resolve_recovery_key(recovery_key)? {
|
||||||
@@ -1042,9 +1242,10 @@ fn handle_sync(
|
|||||||
ContainerAuth::Password(Zeroizing::new(pass))
|
ContainerAuth::Password(Zeroizing::new(pass))
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
let meta = db
|
||||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
.read_meta()
|
||||||
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
let (dek, version, vault_id) = match auth {
|
let (dek, version, vault_id) = match auth {
|
||||||
ContainerAuth::Password(ref password) => {
|
ContainerAuth::Password(ref password) => {
|
||||||
@@ -1054,8 +1255,7 @@ fn handle_sync(
|
|||||||
(keys.dek().clone(), meta.version, keys.slot_id())
|
(keys.dek().clone(), meta.version, keys.slot_id())
|
||||||
}
|
}
|
||||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||||
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")?;
|
|
||||||
let v_id = detect_recovery_key_slot(&db, &dek, meta.version).unwrap_or(0);
|
let v_id = detect_recovery_key_slot(&db, &dek, meta.version).unwrap_or(0);
|
||||||
(dek, meta.version, v_id)
|
(dek, meta.version, v_id)
|
||||||
}
|
}
|
||||||
@@ -1085,34 +1285,35 @@ fn handle_sync(
|
|||||||
|
|
||||||
println!();
|
println!();
|
||||||
if dry_run {
|
if dry_run {
|
||||||
println!(" {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!", ui::yellow("[!]"));
|
println!(
|
||||||
|
" {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!",
|
||||||
|
ui::yellow("[!]")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!(" • Richtung: {}", ui::cyan(dir_str));
|
println!(" • Richtung: {}", ui::cyan(dir_str));
|
||||||
println!(" • Quelle: {}", source);
|
println!(" • Quelle: {}", source);
|
||||||
println!(" • Ziel: {}", target);
|
println!(" • Ziel: {}", target);
|
||||||
if delete {
|
if delete {
|
||||||
println!(" • Spiegelung: {} (Dateien im Ziel ohne Entsprechung werden gelöscht)", ui::red("--delete aktiv"));
|
println!(
|
||||||
|
" • Spiegelung: {} (Dateien im Ziel ohne Entsprechung werden gelöscht)",
|
||||||
|
ui::red("--delete aktiv")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if checksum {
|
if checksum {
|
||||||
println!(" • Modus: Kryptografischer Inhaltsabgleich (--checksum aktiv)");
|
println!(" • Modus: Kryptografischer Inhaltsabgleich (--checksum aktiv)");
|
||||||
}
|
}
|
||||||
if !options.exclude_patterns.is_empty() {
|
if !options.exclude_patterns.is_empty() {
|
||||||
println!(" • Filter: {} Ausschlussmuster", options.exclude_patterns.len());
|
println!(
|
||||||
|
" • Filter: {} Ausschlussmuster",
|
||||||
|
options.exclude_patterns.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
println!(" {} Starte Synchronisation ...", ui::dim("[-]"));
|
println!(" {} Starte Synchronisation ...", ui::dim("[-]"));
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = sanctum::sync::run_sync(
|
let stats = sanctum::sync::run_sync(&db, vault_id, &dek, version, source, target, &options)?;
|
||||||
&db,
|
|
||||||
vault_id,
|
|
||||||
&dek,
|
|
||||||
version,
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
&options,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if !dry_run {
|
if !dry_run {
|
||||||
let _ = db.checkpoint();
|
let _ = db.checkpoint();
|
||||||
@@ -1124,27 +1325,59 @@ fn handle_sync(
|
|||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ ℹ Dry-Run Simulation erfolgreich abgeschlossen │");
|
println!("│ ℹ Dry-Run Simulation erfolgreich abgeschlossen │");
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
println!(
|
||||||
println!(" • Zu übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
" • Gescannt: {} Dateien/Objekte",
|
||||||
println!(" • Bereits aktuell: {} Dateien (übersprungen)", stats.files_skipped);
|
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 {
|
if delete {
|
||||||
println!(" • Zu löschen: {} Dateien/Ordner", stats.files_deleted);
|
println!(
|
||||||
|
" • Zu löschen: {} Dateien/Ordner",
|
||||||
|
stats.files_deleted
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!(" • Hinweis: {} Es wurden keine Änderungen vorgenommen.", ui::yellow("✔"));
|
println!(
|
||||||
|
" • Hinweis: {} Es wurden keine Änderungen vorgenommen.",
|
||||||
|
ui::yellow("✔")
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ ✔ Synchronisation erfolgreich abgeschlossen │");
|
println!("│ ✔ Synchronisation erfolgreich abgeschlossen │");
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
println!(
|
||||||
println!(" • Übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
" • Gescannt: {} Dateien/Objekte",
|
||||||
println!(" • Übersprungen: {} Dateien (bereits aktuell)", stats.files_skipped);
|
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 {
|
if delete {
|
||||||
println!(" • Gelöscht: {} verwaiste Dateien/Ordner", stats.files_deleted);
|
println!(
|
||||||
|
" • Gelöscht: {} verwaiste Dateien/Ordner",
|
||||||
|
stats.files_deleted
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let secs = stats.elapsed.as_secs_f64();
|
let secs = stats.elapsed.as_secs_f64();
|
||||||
let mb = (stats.bytes_transferred as f64) / (1024.0 * 1024.0);
|
let mb = (stats.bytes_transferred as f64) / (1024.0 * 1024.0);
|
||||||
let speed = if secs > 0.05 { mb / secs } else { 0.0 };
|
let speed = if secs > 0.05 { mb / secs } else { 0.0 };
|
||||||
println!(" • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)", secs, speed);
|
println!(
|
||||||
|
" • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)",
|
||||||
|
secs, speed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
@@ -1247,11 +1480,19 @@ async fn run() -> Result<()> {
|
|||||||
Commands::Unmount { drive } => {
|
Commands::Unmount { drive } => {
|
||||||
let drive_char = parse_drive_letter(&drive)?;
|
let drive_char = parse_drive_letter(&drive)?;
|
||||||
let drive_str = format_drive(drive_char);
|
let drive_str = format_drive(drive_char);
|
||||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
print!(
|
||||||
|
" {} Trenne Windows-Netzlaufwerk {} ... ",
|
||||||
|
ui::dim("[-]"),
|
||||||
|
drive_str
|
||||||
|
);
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
unmount_drive(drive_char)?;
|
unmount_drive(drive_char)?;
|
||||||
println!("{}", ui::green("OK"));
|
println!("{}", ui::green("OK"));
|
||||||
println!("{} Laufwerk {} erfolgreich getrennt.", ui::green("✔"), drive_str);
|
println!(
|
||||||
|
"{} Laufwerk {} erfolgreich getrennt.",
|
||||||
|
ui::green("✔"),
|
||||||
|
drive_str
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Commands::Passwd { path, recovery_key } => {
|
Commands::Passwd { path, recovery_key } => {
|
||||||
handle_passwd(&path, recovery_key.as_deref())?;
|
handle_passwd(&path, recovery_key.as_deref())?;
|
||||||
@@ -1304,7 +1545,10 @@ async fn run() -> Result<()> {
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
sanctum::windows::register_explorer_integration()?;
|
sanctum::windows::register_explorer_integration()?;
|
||||||
println!(" {} .sanctum-Dateien wurden erfolgreich im Windows Explorer verknüpft!", ui::green("✔"));
|
println!(
|
||||||
|
" {} .sanctum-Dateien wurden erfolgreich im Windows Explorer verknüpft!",
|
||||||
|
ui::green("✔")
|
||||||
|
);
|
||||||
println!(" • Doppelklick: Öffnet und bindet den Container direkt als Laufwerk ein");
|
println!(" • Doppelklick: Öffnet und bindet den Container direkt als Laufwerk ein");
|
||||||
println!(" • Rechtsklick: Bietet Optionen für Integritätsprüfung (FSCK) und Header-Backup");
|
println!(" • Rechtsklick: Bietet Optionen für Integritätsprüfung (FSCK) und Header-Backup");
|
||||||
println!();
|
println!();
|
||||||
@@ -1315,10 +1559,19 @@ async fn run() -> Result<()> {
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
sanctum::windows::unregister_explorer_integration()?;
|
sanctum::windows::unregister_explorer_integration()?;
|
||||||
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, insecure_url } => {
|
Commands::Upgrade {
|
||||||
|
check,
|
||||||
|
yes,
|
||||||
|
force,
|
||||||
|
url,
|
||||||
|
insecure_url,
|
||||||
|
} => {
|
||||||
let options = sanctum::upgrade::UpgradeOptions {
|
let options = sanctum::upgrade::UpgradeOptions {
|
||||||
check_only: check,
|
check_only: check,
|
||||||
yes,
|
yes,
|
||||||
@@ -1333,7 +1586,6 @@ async fn run() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren
|
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren
|
||||||
@@ -1353,4 +1605,3 @@ async fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+134
-80
@@ -61,9 +61,7 @@ fn run_mount_command(drive_letter: char, port: u16, session_token: &str) -> Resu
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let dav_url = format!("dav://127.0.0.1:{}/", port);
|
let dav_url = format!("dav://127.0.0.1:{}/", port);
|
||||||
let _ = Command::new("gio")
|
let _ = Command::new("gio").args(["mount", &dav_url]).output();
|
||||||
.args(["mount", &dav_url])
|
|
||||||
.output();
|
|
||||||
let _ = (drive_letter, session_token);
|
let _ = (drive_letter, session_token);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -106,8 +104,7 @@ pub async fn mount_container(
|
|||||||
println!();
|
println!();
|
||||||
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
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")?;
|
|
||||||
|
|
||||||
let meta = db
|
let meta = db
|
||||||
.read_meta()
|
.read_meta()
|
||||||
@@ -116,12 +113,22 @@ 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) => {
|
||||||
if !stealth {
|
if !stealth {
|
||||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...");
|
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) => {
|
||||||
if !stealth {
|
if !stealth {
|
||||||
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
|
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();
|
||||||
@@ -138,8 +145,7 @@ pub async fn mount_container(
|
|||||||
if !stealth {
|
if !stealth {
|
||||||
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
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")?;
|
|
||||||
if !stealth {
|
if !stealth {
|
||||||
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
||||||
}
|
}
|
||||||
@@ -152,10 +158,12 @@ pub async fn mount_container(
|
|||||||
println!(" [i] Der angegebene Notfallschlüssel gehört zum Hidden-Vault (Slot 1).");
|
println!(" [i] Der angegebene Notfallschlüssel gehört zum Hidden-Vault (Slot 1).");
|
||||||
println!(" Für den Zugriff auf die Trägerdatei wird das Passwort des Standard-Vaults benötigt.");
|
println!(" Für den Zugriff auf die Trägerdatei wird das Passwort des Standard-Vaults benötigt.");
|
||||||
}
|
}
|
||||||
let decoy_pass = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
let decoy_pass =
|
||||||
.context("Fehler beim Einlesen des Standard-Vault Passworts")?;
|
rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||||
let decoy_keys = meta.authenticate(&decoy_pass)
|
.context("Fehler beim Einlesen des Standard-Vault Passworts")?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("Ungültiges Passwort für Standard-Vault."))?;
|
let decoy_keys = meta.authenticate(&decoy_pass).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Ungültiges Passwort für Standard-Vault.")
|
||||||
|
})?;
|
||||||
(Some(decoy_keys.dek().clone()), Some(carrier_node_id), 1)
|
(Some(decoy_keys.dek().clone()), Some(carrier_node_id), 1)
|
||||||
} else {
|
} else {
|
||||||
(None, Some(carrier_node_id), 0)
|
(None, Some(carrier_node_id), 0)
|
||||||
@@ -164,7 +172,13 @@ pub async fn mount_container(
|
|||||||
(None, None, 0)
|
(None, None, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
(dek, carrier_dek, final_carrier_node_id, meta.version, vault_id)
|
(
|
||||||
|
dek,
|
||||||
|
carrier_dek,
|
||||||
|
final_carrier_node_id,
|
||||||
|
meta.version,
|
||||||
|
vault_id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -340,7 +354,10 @@ pub async fn mount_container(
|
|||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
println!(
|
||||||
|
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
|
||||||
|
ui::cyan(&drive_str)
|
||||||
|
);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
if let Some(mp) = mount_point {
|
if let Some(mp) = mount_point {
|
||||||
@@ -349,12 +366,22 @@ pub async fn mount_container(
|
|||||||
println!(" • Modus: WebDAV Userland-VFS");
|
println!(" • Modus: WebDAV Userland-VFS");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/ (lokal geschützt)", bound_port);
|
println!(
|
||||||
|
" • WebDAV-URL: http://127.0.0.1:{}/ (lokal geschützt)",
|
||||||
|
bound_port
|
||||||
|
);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/", bound_port);
|
println!(
|
||||||
|
" • gio Befehl: gio mount dav://127.0.0.1:{}/",
|
||||||
|
bound_port
|
||||||
|
);
|
||||||
if let Some(mp) = mount_point {
|
if let Some(mp) = mount_point {
|
||||||
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/ {}", bound_port, mp.display());
|
println!(
|
||||||
|
" • davfs2: mount -t davfs http://127.0.0.1:{}/ {}",
|
||||||
|
bound_port,
|
||||||
|
mp.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(secs) = idle_timeout {
|
if let Some(secs) = idle_timeout {
|
||||||
@@ -376,9 +403,15 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
println!(
|
||||||
|
" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.",
|
||||||
|
ui::yellow("Tipp")
|
||||||
|
);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
println!(" [{}] Drücke [Ctrl+C] zum sicheren Beenden.", ui::yellow("Tipp"));
|
println!(
|
||||||
|
" [{}] Drücke [Ctrl+C] zum sicheren Beenden.",
|
||||||
|
ui::yellow("Tipp")
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,11 +419,13 @@ pub async fn mount_container(
|
|||||||
#[cfg(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)]
|
#[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)
|
// Unix Signale (SIGTERM, SIGHUP)
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
|
let mut sigterm =
|
||||||
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
|
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
|
||||||
|
|
||||||
@@ -503,7 +538,11 @@ pub async fn mount_container(
|
|||||||
} else {
|
} else {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
print!(
|
||||||
|
" {} Trenne Windows-Netzlaufwerk {} ... ",
|
||||||
|
ui::dim("[-]"),
|
||||||
|
drive_str
|
||||||
|
);
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
|
||||||
// Automatisches Unmount
|
// Automatisches Unmount
|
||||||
@@ -527,7 +566,11 @@ pub async fn mount_container(
|
|||||||
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
|
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
|
||||||
if let Ok(freelist) = db.freelist_count() {
|
if let Ok(freelist) = db.freelist_count() {
|
||||||
if freelist > 0 {
|
if freelist > 0 {
|
||||||
print!(" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist);
|
print!(
|
||||||
|
" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ",
|
||||||
|
ui::dim("[-]"),
|
||||||
|
freelist
|
||||||
|
);
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
match db.incremental_vacuum(None) {
|
match db.incremental_vacuum(None) {
|
||||||
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
|
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
|
||||||
@@ -546,7 +589,10 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔"));
|
println!(
|
||||||
|
"{} Sanctum Container wurde sicher und vollständig geschlossen.",
|
||||||
|
ui::green("✔")
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,6 +662,7 @@ const HTTP_HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_
|
|||||||
|
|
||||||
/// Hilfsfunktion zur Validierung von HTTP Basic Auth.
|
/// Hilfsfunktion zur Validierung von HTTP Basic Auth.
|
||||||
/// Unterstützt Format `username:password` (wobei Passwort dem Session-Token entspricht).
|
/// Unterstützt Format `username:password` (wobei Passwort dem Session-Token entspricht).
|
||||||
|
/// SA-06: Verwendet strikten Constant-Time-Vergleich gegen Timing-Side-Channel-Angriffe.
|
||||||
pub fn check_basic_auth(auth_header: &str, expected_token: &str) -> bool {
|
pub fn check_basic_auth(auth_header: &str, expected_token: &str) -> bool {
|
||||||
let auth_str = auth_header.trim();
|
let auth_str = auth_header.trim();
|
||||||
let encoded = if let Some(rest) = auth_str.strip_prefix("Basic ") {
|
let encoded = if let Some(rest) = auth_str.strip_prefix("Basic ") {
|
||||||
@@ -637,44 +684,36 @@ pub fn check_basic_auth(auth_header: &str, expected_token: &str) -> bool {
|
|||||||
Err(_) => return false,
|
Err(_) => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
if let Some((_user, pass)) = decoded_str.split_once(':') {
|
if let Some((_user, pass)) = decoded_str.split_once(':') {
|
||||||
pass == expected_token
|
pass.as_bytes().ct_eq(expected_token.as_bytes()).into()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hilfsfunktion zur Validierung des X-Sanctum-Token Headers.
|
/// Hilfsfunktion zur Validierung des X-Sanctum-Token Headers.
|
||||||
|
/// SA-06: Verwendet strikten Constant-Time-Vergleich gegen Timing-Side-Channel-Angriffe.
|
||||||
pub fn check_token_header(headers: &hyper::HeaderMap, expected_token: &str) -> bool {
|
pub fn check_token_header(headers: &hyper::HeaderMap, expected_token: &str) -> bool {
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
if let Some(val) = headers.get("X-Sanctum-Token") {
|
if let Some(val) = headers.get("X-Sanctum-Token") {
|
||||||
if let Ok(val_str) = val.to_str() {
|
if let Ok(val_str) = val.to_str() {
|
||||||
return val_str.trim() == expected_token;
|
return val_str
|
||||||
|
.trim()
|
||||||
|
.as_bytes()
|
||||||
|
.ct_eq(expected_token.as_bytes())
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Schneidet ein Pfad-Präfix /<session_token> aus der Request-URI heraus (Defense-in-Depth Fallback).
|
/// Prüft, ob eine URI oder deren Query-Parameter das sensible Session-Token enthält (SA-05 / CWE-598).
|
||||||
pub fn strip_path_prefix(uri: &hyper::Uri, prefix: &str) -> Option<hyper::Uri> {
|
pub fn uri_contains_token(uri: &hyper::Uri, session_token: &str) -> bool {
|
||||||
let path = uri.path();
|
if session_token.is_empty() {
|
||||||
if !path.starts_with(prefix) {
|
return false;
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let rest = &path[prefix.len()..];
|
uri.path().contains(session_token) || uri.query().unwrap_or("").contains(session_token)
|
||||||
let new_path = if rest.is_empty() || !rest.starts_with('/') {
|
|
||||||
format!("/{}", rest)
|
|
||||||
} else {
|
|
||||||
rest.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let path_and_query = match uri.query() {
|
|
||||||
Some(q) => format!("{}?{}", new_path, q),
|
|
||||||
None => new_path,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut parts = uri.clone().into_parts();
|
|
||||||
parts.path_and_query = Some(path_and_query.parse().ok()?);
|
|
||||||
hyper::Uri::from_parts(parts).ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Erzeugt eine standardkonforme HTTP 401 Unauthorized Antwort mit WWW-Authenticate Header.
|
/// Erzeugt eine standardkonforme HTTP 401 Unauthorized Antwort mit WWW-Authenticate Header.
|
||||||
@@ -687,15 +726,15 @@ fn unauthorized_response() -> hyper::Response<dav_server::body::Body> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Multi-Auth & Host-Header Sicherheits-Middleware aus.
|
/// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Header-basierter Authentifizierung & Host-Header Sicherheits-Middleware aus.
|
||||||
pub async fn serve_webdav_loop(
|
pub async fn serve_webdav_loop(
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
dav_server: DavHandler,
|
dav_server: DavHandler,
|
||||||
session_token: String,
|
session_token: String,
|
||||||
mut shutdown_rx: watch::Receiver<bool>,
|
mut shutdown_rx: watch::Receiver<bool>,
|
||||||
) {
|
) {
|
||||||
let conn_semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_DAV_CONNECTIONS));
|
let conn_semaphore =
|
||||||
let token_prefix = format!("/{}", session_token);
|
std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_DAV_CONNECTIONS));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -724,15 +763,13 @@ pub async fn serve_webdav_loop(
|
|||||||
let io = TokioIo::new(stream);
|
let io = TokioIo::new(stream);
|
||||||
let handler = dav_server.clone();
|
let handler = dav_server.clone();
|
||||||
let expected_token = session_token.clone();
|
let expected_token = session_token.clone();
|
||||||
let expected_prefix = token_prefix.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _permit = permit; // Permit wird bei Verbindungsende automatisch freigegeben
|
let _permit = permit; // Permit wird bei Verbindungsende automatisch freigegeben
|
||||||
|
|
||||||
let service = service_fn(move |mut req| {
|
let service = service_fn(move |req| {
|
||||||
let h = handler.clone();
|
let h = handler.clone();
|
||||||
let token = expected_token.clone();
|
let token = expected_token.clone();
|
||||||
let prefix = expected_prefix.clone();
|
|
||||||
async move {
|
async move {
|
||||||
// 1. RT-02: Strikte Fail-Closed Host-Header Validierung (Anti-DNS-Rebinding & Anti-Spoofing)
|
// 1. RT-02: Strikte Fail-Closed Host-Header Validierung (Anti-DNS-Rebinding & Anti-Spoofing)
|
||||||
let host_valid = match req.headers().get(hyper::header::HOST) {
|
let host_valid = match req.headers().get(hyper::header::HOST) {
|
||||||
@@ -750,15 +787,27 @@ pub async fn serve_webdav_loop(
|
|||||||
);
|
);
|
||||||
let res = hyper::Response::builder()
|
let res = hyper::Response::builder()
|
||||||
.status(hyper::StatusCode::FORBIDDEN)
|
.status(hyper::StatusCode::FORBIDDEN)
|
||||||
|
.header(hyper::header::CONTENT_LENGTH, "0")
|
||||||
.body(dav_server::body::Body::empty())
|
.body(dav_server::body::Body::empty())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
return Ok::<_, Infallible>(res);
|
return Ok::<_, Infallible>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. R-05: Multi-Auth Middleware
|
// 2. SA-05: Verhindere Session-Token in Request-URI oder Query (CWE-598).
|
||||||
|
// Session-Tokens dürfen ausschließlich in HTTP-Headern übertragen werden.
|
||||||
|
if uri_contains_token(req.uri(), &token) {
|
||||||
|
warn!("Abgewiesener Zugriff: Session-Token in URI/Query übermittelt (SA-05 / CWE-598)");
|
||||||
|
let res = hyper::Response::builder()
|
||||||
|
.status(hyper::StatusCode::FORBIDDEN)
|
||||||
|
.header(hyper::header::CONTENT_LENGTH, "0")
|
||||||
|
.body(dav_server::body::Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
return Ok::<_, Infallible>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SA-05 & SA-06: Header-basierte Authentifizierung mit Constant-Time Token-Vergleich
|
||||||
// a) HTTP Basic Auth (Authorization: Basic ...)
|
// a) HTTP Basic Auth (Authorization: Basic ...)
|
||||||
// b) Header X-Sanctum-Token
|
// b) Header X-Sanctum-Token
|
||||||
// c) Pfad-Präfix Fallback (/<session_token>/...)
|
|
||||||
let mut authenticated = false;
|
let mut authenticated = false;
|
||||||
|
|
||||||
if let Some(auth_val) = req.headers().get(hyper::header::AUTHORIZATION) {
|
if let Some(auth_val) = req.headers().get(hyper::header::AUTHORIZATION) {
|
||||||
@@ -773,15 +822,6 @@ pub async fn serve_webdav_loop(
|
|||||||
authenticated = true;
|
authenticated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authenticated {
|
|
||||||
if let Some(rewritten_uri) = strip_path_prefix(req.uri(), &prefix) {
|
|
||||||
*req.uri_mut() = rewritten_uri;
|
|
||||||
authenticated = true;
|
|
||||||
}
|
|
||||||
} else if let Some(rewritten_uri) = strip_path_prefix(req.uri(), &prefix) {
|
|
||||||
*req.uri_mut() = rewritten_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
debug!("Abgewiesener unauthentifizierter Zugriff auf: {}", req.uri().path());
|
debug!("Abgewiesener unauthentifizierter Zugriff auf: {}", req.uri().path());
|
||||||
return Ok::<_, Infallible>(unauthorized_response());
|
return Ok::<_, Infallible>(unauthorized_response());
|
||||||
@@ -841,15 +881,24 @@ mod tests {
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let token = "deadbeefcafebabe0123456789abcdef";
|
let token = "deadbeefcafebabe0123456789abcdef";
|
||||||
let creds = format!("sanctum:{}", token);
|
let creds = format!("sanctum:{}", token);
|
||||||
let header_val = format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(creds));
|
let header_val = format!(
|
||||||
|
"Basic {}",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(creds)
|
||||||
|
);
|
||||||
assert!(check_basic_auth(&header_val, token));
|
assert!(check_basic_auth(&header_val, token));
|
||||||
|
|
||||||
// Kleingeschriebenes basic Präfix
|
// Kleingeschriebenes basic Präfix
|
||||||
let lower_header = format!("basic {}", base64::engine::general_purpose::STANDARD.encode(format!("user:{}", token)));
|
let lower_header = format!(
|
||||||
|
"basic {}",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(format!("user:{}", token))
|
||||||
|
);
|
||||||
assert!(check_basic_auth(&lower_header, token));
|
assert!(check_basic_auth(&lower_header, token));
|
||||||
|
|
||||||
// Falsches Token
|
// Falsches Token
|
||||||
let wrong_token_header = format!("Basic {}", base64::engine::general_purpose::STANDARD.encode("sanctum:wrongtoken"));
|
let wrong_token_header = format!(
|
||||||
|
"Basic {}",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode("sanctum:wrongtoken")
|
||||||
|
);
|
||||||
assert!(!check_basic_auth(&wrong_token_header, token));
|
assert!(!check_basic_auth(&wrong_token_header, token));
|
||||||
|
|
||||||
// Ungültiges Base64 oder Format
|
// Ungültiges Base64 oder Format
|
||||||
@@ -873,22 +922,27 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_strip_path_prefix() {
|
fn test_uri_contains_token_rejection() {
|
||||||
let token = "deadbeefcafebabe0123456789abcdef";
|
let token = "deadbeefcafebabe0123456789abcdef";
|
||||||
let prefix = format!("/{}", token);
|
|
||||||
|
|
||||||
let uri: hyper::Uri = format!("http://127.0.0.1:8443/{}/Photos/vacation.jpg?sort=date", token).parse().unwrap();
|
// Token im Pfad
|
||||||
let stripped = strip_path_prefix(&uri, &prefix).expect("Should strip prefix");
|
let uri_path: hyper::Uri = format!("http://127.0.0.1:8443/{}/Photos/vacation.jpg", token)
|
||||||
assert_eq!(stripped.path(), "/Photos/vacation.jpg");
|
.parse()
|
||||||
assert_eq!(stripped.query(), Some("sort=date"));
|
.unwrap();
|
||||||
|
assert!(uri_contains_token(&uri_path, token));
|
||||||
|
|
||||||
let uri_root: hyper::Uri = format!("http://127.0.0.1:8443/{}", token).parse().unwrap();
|
// Token im Query-String
|
||||||
let stripped_root = strip_path_prefix(&uri_root, &prefix).expect("Should strip root");
|
let uri_query: hyper::Uri =
|
||||||
assert_eq!(stripped_root.path(), "/");
|
format!("http://127.0.0.1:8443/Photos/vacation.jpg?token={}", token)
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
assert!(uri_contains_token(&uri_query, token));
|
||||||
|
|
||||||
let uri_no_prefix: hyper::Uri = "http://127.0.0.1:8443/other/path".parse().unwrap();
|
// Harmloser Pfad ohne Token
|
||||||
assert!(strip_path_prefix(&uri_no_prefix, &prefix).is_none());
|
let uri_clean: hyper::Uri = "http://127.0.0.1:8443/Photos/vacation.jpg".parse().unwrap();
|
||||||
|
assert!(!uri_contains_token(&uri_clean, token));
|
||||||
|
|
||||||
|
// Leeres Token darf niemals matchen
|
||||||
|
assert!(!uri_contains_token(&uri_clean, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
/// Validiert einen Datei- oder Verzeichnisnamen gegen Path-Traversal, Null-Bytes,
|
/// Validiert einen Datei- oder Verzeichnisnamen gegen Path-Traversal, Null-Bytes,
|
||||||
/// unzulässige Steuerzeichen und Windows-reservierte Gerätenamen (S-08, R-03).
|
/// unzulässige Steuerzeichen und Windows-reservierte Gerätenamen (S-08, R-03).
|
||||||
@@ -7,7 +7,10 @@ pub fn validate_node_name(name: &str) -> Result<()> {
|
|||||||
bail!("Dateiname darf nicht leer sein.");
|
bail!("Dateiname darf nicht leer sein.");
|
||||||
}
|
}
|
||||||
if name == "." || name == ".." {
|
if name == "." || name == ".." {
|
||||||
bail!("Ungültiger Dateiname (Verzeichnisreferenz verboten): '{}'", name);
|
bail!(
|
||||||
|
"Ungültiger Dateiname (Verzeichnisreferenz verboten): '{}'",
|
||||||
|
name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if name.contains('/') || name.contains('\\') || name.contains('\0') {
|
if name.contains('/') || name.contains('\\') || name.contains('\0') {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -29,9 +32,8 @@ pub fn validate_node_name(name: &str) -> Result<()> {
|
|||||||
};
|
};
|
||||||
let base_upper = base_name.to_ascii_uppercase();
|
let base_upper = base_name.to_ascii_uppercase();
|
||||||
let reserved = [
|
let reserved = [
|
||||||
"CON", "PRN", "AUX", "NUL",
|
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
|
||||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
|
||||||
];
|
];
|
||||||
if reserved.contains(&base_upper.as_str()) {
|
if reserved.contains(&base_upper.as_str()) {
|
||||||
bail!(
|
bail!(
|
||||||
|
|||||||
+114
-34
@@ -85,11 +85,31 @@ impl HeaderBackup {
|
|||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
|
|
||||||
if !self.slots.is_empty() {
|
if !self.slots.is_empty() {
|
||||||
|
if self.slots.len() > 2 {
|
||||||
|
bail!(
|
||||||
|
"Ungültige Slot-Anzahl im Backup: {} (maximal 2 erlaubt)",
|
||||||
|
self.slots.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut seen_ids = std::collections::HashSet::new();
|
||||||
for s in &self.slots {
|
for s in &self.slots {
|
||||||
let salt_bytes = hex::decode(&s.kdf_salt_hex)
|
if s.slot_id > 1 {
|
||||||
.context("Ungültige Hex-Kodierung für KDF-Salt")?;
|
bail!("Ungültige Slot-ID {} im Backup: Es sind ausschließlich die Slot-IDs 0 und 1 erlaubt", s.slot_id);
|
||||||
|
}
|
||||||
|
if !seen_ids.insert(s.slot_id) {
|
||||||
|
bail!("Doppelte Slot-ID {} im Backup entdeckt", s.slot_id);
|
||||||
|
}
|
||||||
|
crate::crypto::validate_kdf_params(&s.kdf_params).map_err(|e| {
|
||||||
|
anyhow::anyhow!("KDF-Parameter in Slot {} ungültig: {e}", s.slot_id)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let salt_bytes =
|
||||||
|
hex::decode(&s.kdf_salt_hex).context("Ungültige Hex-Kodierung für KDF-Salt")?;
|
||||||
if salt_bytes.len() != 16 {
|
if salt_bytes.len() != 16 {
|
||||||
bail!("Ungültige Salt-Länge im Backup: erwartet 16 Bytes, erhalten {}", salt_bytes.len());
|
bail!(
|
||||||
|
"Ungültige Salt-Länge im Backup: erwartet 16 Bytes, erhalten {}",
|
||||||
|
salt_bytes.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut kdf_salt = [0u8; 16];
|
let mut kdf_salt = [0u8; 16];
|
||||||
kdf_salt.copy_from_slice(&salt_bytes);
|
kdf_salt.copy_from_slice(&salt_bytes);
|
||||||
@@ -97,10 +117,24 @@ impl HeaderBackup {
|
|||||||
let wrapped_dek = hex::decode(&s.wrapped_dek_hex)
|
let wrapped_dek = hex::decode(&s.wrapped_dek_hex)
|
||||||
.context("Ungültige Hex-Kodierung für wrapped_dek")?;
|
.context("Ungültige Hex-Kodierung für wrapped_dek")?;
|
||||||
|
|
||||||
|
if s.slot_id == 0 {
|
||||||
|
if wrapped_dek.len() != 40 && wrapped_dek.len() != 32 {
|
||||||
|
bail!("Ungültige wrapped_dek-Länge in Slot 0: {} Bytes (erwartet: 40 oder 32)", wrapped_dek.len());
|
||||||
|
}
|
||||||
|
} else if s.slot_id == 1 {
|
||||||
|
if wrapped_dek.len() != 72 && wrapped_dek.len() != 64 && wrapped_dek.len() != 32
|
||||||
|
{
|
||||||
|
bail!("Ungültige wrapped_dek-Länge in Slot 1: {} Bytes (erwartet: 72, 64 oder 32)", wrapped_dek.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let nonce_bytes = hex::decode(&s.header_nonce_hex)
|
let nonce_bytes = hex::decode(&s.header_nonce_hex)
|
||||||
.context("Ungültige Hex-Kodierung für Header-Nonce")?;
|
.context("Ungültige Hex-Kodierung für Header-Nonce")?;
|
||||||
if nonce_bytes.len() != 12 {
|
if nonce_bytes.len() != 12 {
|
||||||
bail!("Ungültige Nonce-Länge im Backup: erwartet 12 Bytes, erhalten {}", nonce_bytes.len());
|
bail!(
|
||||||
|
"Ungültige Nonce-Länge im Backup: erwartet 12 Bytes, erhalten {}",
|
||||||
|
nonce_bytes.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut header_nonce = [0u8; 12];
|
let mut header_nonce = [0u8; 12];
|
||||||
header_nonce.copy_from_slice(&nonce_bytes);
|
header_nonce.copy_from_slice(&nonce_bytes);
|
||||||
@@ -108,7 +142,10 @@ impl HeaderBackup {
|
|||||||
let tag_bytes = hex::decode(&s.header_tag_hex)
|
let tag_bytes = hex::decode(&s.header_tag_hex)
|
||||||
.context("Ungültige Hex-Kodierung für Header-Tag")?;
|
.context("Ungültige Hex-Kodierung für Header-Tag")?;
|
||||||
if tag_bytes.len() != 16 {
|
if tag_bytes.len() != 16 {
|
||||||
bail!("Ungültige Tag-Länge im Backup: erwartet 16 Bytes, erhalten {}", tag_bytes.len());
|
bail!(
|
||||||
|
"Ungültige Tag-Länge im Backup: erwartet 16 Bytes, erhalten {}",
|
||||||
|
tag_bytes.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut header_tag = [0u8; 16];
|
let mut header_tag = [0u8; 16];
|
||||||
header_tag.copy_from_slice(&tag_bytes);
|
header_tag.copy_from_slice(&tag_bytes);
|
||||||
@@ -123,12 +160,19 @@ impl HeaderBackup {
|
|||||||
header_tag,
|
header_tag,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !seen_ids.contains(&0) {
|
||||||
|
bail!("Ungültiges Backup: Slot 0 (Standard/Decoy Vault) fehlt");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback für alte Backups ohne slots-Array
|
// Fallback für alte Backups ohne slots-Array
|
||||||
let salt_bytes = hex::decode(&self.kdf_salt_hex)
|
let salt_bytes =
|
||||||
.context("Ungültige Hex-Kodierung für KDF-Salt")?;
|
hex::decode(&self.kdf_salt_hex).context("Ungültige Hex-Kodierung für KDF-Salt")?;
|
||||||
if salt_bytes.len() != 16 {
|
if salt_bytes.len() != 16 {
|
||||||
bail!("Ungültige Salt-Länge im Backup: erwartet 16 Bytes, erhalten {}", salt_bytes.len());
|
bail!(
|
||||||
|
"Ungültige Salt-Länge im Backup: erwartet 16 Bytes, erhalten {}",
|
||||||
|
salt_bytes.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut kdf_salt = [0u8; 16];
|
let mut kdf_salt = [0u8; 16];
|
||||||
kdf_salt.copy_from_slice(&salt_bytes);
|
kdf_salt.copy_from_slice(&salt_bytes);
|
||||||
@@ -139,7 +183,10 @@ impl HeaderBackup {
|
|||||||
let nonce_bytes = hex::decode(&self.header_nonce_hex)
|
let nonce_bytes = hex::decode(&self.header_nonce_hex)
|
||||||
.context("Ungültige Hex-Kodierung für Header-Nonce")?;
|
.context("Ungültige Hex-Kodierung für Header-Nonce")?;
|
||||||
if nonce_bytes.len() != 12 {
|
if nonce_bytes.len() != 12 {
|
||||||
bail!("Ungültige Nonce-Länge im Backup: erwartet 12 Bytes, erhalten {}", nonce_bytes.len());
|
bail!(
|
||||||
|
"Ungültige Nonce-Länge im Backup: erwartet 12 Bytes, erhalten {}",
|
||||||
|
nonce_bytes.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut header_nonce = [0u8; 12];
|
let mut header_nonce = [0u8; 12];
|
||||||
header_nonce.copy_from_slice(&nonce_bytes);
|
header_nonce.copy_from_slice(&nonce_bytes);
|
||||||
@@ -147,7 +194,10 @@ impl HeaderBackup {
|
|||||||
let tag_bytes = hex::decode(&self.header_tag_hex)
|
let tag_bytes = hex::decode(&self.header_tag_hex)
|
||||||
.context("Ungültige Hex-Kodierung für Header-Tag")?;
|
.context("Ungültige Hex-Kodierung für Header-Tag")?;
|
||||||
if tag_bytes.len() != 16 {
|
if tag_bytes.len() != 16 {
|
||||||
bail!("Ungültige Tag-Länge im Backup: erwartet 16 Bytes, erhalten {}", tag_bytes.len());
|
bail!(
|
||||||
|
"Ungültige Tag-Länge im Backup: erwartet 16 Bytes, erhalten {}",
|
||||||
|
tag_bytes.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut header_tag = [0u8; 16];
|
let mut header_tag = [0u8; 16];
|
||||||
header_tag.copy_from_slice(&tag_bytes);
|
header_tag.copy_from_slice(&tag_bytes);
|
||||||
@@ -180,19 +230,28 @@ impl HeaderBackup {
|
|||||||
/// Sichert die Header-Metadaten eines Containers in eine externe Backup-Datei (.sanctum.hdr).
|
/// Sichert die Header-Metadaten eines Containers in eine externe Backup-Datei (.sanctum.hdr).
|
||||||
pub fn export_header_backup(container_path: &Path, backup_path: &Path) -> Result<()> {
|
pub fn export_header_backup(container_path: &Path, backup_path: &Path) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte Container-Datenbank zum Lesen des Headers nicht öffnen")?;
|
.context("Konnte Container-Datenbank zum Lesen des Headers nicht öffnen")?;
|
||||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
let meta = db
|
||||||
|
.read_meta()
|
||||||
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
let backup = HeaderBackup::from_meta(&meta);
|
let backup = HeaderBackup::from_meta(&meta);
|
||||||
let json_data = serde_json::to_string_pretty(&backup)
|
let json_data = serde_json::to_string_pretty(&backup)
|
||||||
.context("Fehler beim Serialisieren des Header-Backups")?;
|
.context("Fehler beim Serialisieren des Header-Backups")?;
|
||||||
|
|
||||||
fs::write(backup_path, json_data)
|
fs::write(backup_path, json_data).with_context(|| {
|
||||||
.with_context(|| format!("Konnte Backup-Datei '{}' nicht schreiben", backup_path.display()))?;
|
format!(
|
||||||
|
"Konnte Backup-Datei '{}' nicht schreiben",
|
||||||
|
backup_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -203,21 +262,25 @@ pub fn restore_header_backup(container_path: &Path, backup_path: &Path) -> Resul
|
|||||||
bail!("Backup-Datei '{}' existiert nicht.", backup_path.display());
|
bail!("Backup-Datei '{}' existiert nicht.", backup_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(backup_path)
|
let content = fs::read_to_string(backup_path).with_context(|| {
|
||||||
.with_context(|| format!("Konnte Backup-Datei '{}' nicht lesen", backup_path.display()))?;
|
format!(
|
||||||
|
"Konnte Backup-Datei '{}' nicht lesen",
|
||||||
|
backup_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let backup: HeaderBackup = serde_json::from_str(&content)
|
let backup: HeaderBackup = serde_json::from_str(&content)
|
||||||
.context("Ungültiges Backup-Dateiformat (JSON-Parsing fehlgeschlagen)")?;
|
.context("Ungültiges Backup-Dateiformat (JSON-Parsing fehlgeschlagen)")?;
|
||||||
|
|
||||||
let meta = backup.to_meta()?;
|
let meta = backup.to_meta()?;
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).context("Konnte Ziel-Containerdatei nicht öffnen")?;
|
||||||
.context("Konnte Ziel-Containerdatei nicht öffnen")?;
|
|
||||||
|
|
||||||
db.restore_meta(&meta)
|
db.restore_meta(&meta)
|
||||||
.context("Fehler beim Wiederherstellen der Header-Tabelle in der Datenbank")?;
|
.context("Fehler beim Wiederherstellen der Header-Tabelle in der Datenbank")?;
|
||||||
|
|
||||||
db.checkpoint().context("Fehler beim WAL-Checkpoint nach Header-Wiederherstellung")?;
|
db.checkpoint()
|
||||||
|
.context("Fehler beim WAL-Checkpoint nach Header-Wiederherstellung")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -256,14 +319,16 @@ pub fn restore_slot_from_recovery_key(
|
|||||||
dek_0: Option<&[u8; 32]>,
|
dek_0: Option<&[u8; 32]>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_password(new_password)?;
|
validate_password(new_password)?;
|
||||||
|
|
||||||
// 1. DEK aus 24-Wort-Phrase dekodieren & validieren
|
// 1. DEK aus 24-Wort-Phrase dekodieren & validieren
|
||||||
let dek = mnemonic_to_dek(recovery_key)
|
let dek = mnemonic_to_dek(recovery_key).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
|
||||||
|
|
||||||
// 2. Neuen KEK mit frischem Salt ableiten
|
// 2. Neuen KEK mit frischem Salt ableiten
|
||||||
let mut salt = [0u8; 16];
|
let mut salt = [0u8; 16];
|
||||||
@@ -273,8 +338,7 @@ pub fn restore_slot_from_recovery_key(
|
|||||||
let kek = derive_kek(new_password, &salt, &kdf_params)
|
let kek = derive_kek(new_password, &salt, &kdf_params)
|
||||||
.context("Schlüsselableitung für neues Passwort fehlgeschlagen")?;
|
.context("Schlüsselableitung für neues Passwort fehlgeschlagen")?;
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
|
||||||
|
|
||||||
let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0);
|
let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0);
|
||||||
|
|
||||||
@@ -347,7 +411,8 @@ pub fn restore_slot_from_recovery_key(
|
|||||||
db.restore_meta(&meta)
|
db.restore_meta(&meta)
|
||||||
.context("Fehler beim Schreiben des rekonstruierten Headers")?;
|
.context("Fehler beim Schreiben des rekonstruierten Headers")?;
|
||||||
|
|
||||||
db.checkpoint().context("Fehler beim WAL-Checkpoint nach Header-Rekonstruktion")?;
|
db.checkpoint()
|
||||||
|
.context("Fehler beim WAL-Checkpoint nach Header-Rekonstruktion")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -370,8 +435,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_header_backup_and_restore() {
|
fn test_header_backup_and_restore() {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let container_path: PathBuf = temp_dir.join(format!("test_backup_{}.sanctum", std::process::id()));
|
let container_path: PathBuf =
|
||||||
let backup_path: PathBuf = temp_dir.join(format!("test_backup_{}.sanctum.hdr", std::process::id()));
|
temp_dir.join(format!("test_backup_{}.sanctum", std::process::id()));
|
||||||
|
let backup_path: PathBuf =
|
||||||
|
temp_dir.join(format!("test_backup_{}.sanctum.hdr", std::process::id()));
|
||||||
|
|
||||||
if container_path.exists() {
|
if container_path.exists() {
|
||||||
let _ = fs::remove_file(&container_path);
|
let _ = fs::remove_file(&container_path);
|
||||||
@@ -392,7 +459,8 @@ mod tests {
|
|||||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&container_path).unwrap();
|
let db = Database::open(&container_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
// 1. Export
|
// 1. Export
|
||||||
@@ -416,8 +484,13 @@ mod tests {
|
|||||||
let restored_db = Database::open(&container_path).unwrap();
|
let restored_db = Database::open(&container_path).unwrap();
|
||||||
let meta = restored_db.read_meta().expect("Read restored meta");
|
let meta = restored_db.read_meta().expect("Read restored meta");
|
||||||
let restored_kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params).unwrap();
|
let restored_kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params).unwrap();
|
||||||
let active_dek = unwrap_dek(&restored_kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag)
|
let active_dek = unwrap_dek(
|
||||||
.expect("Unwrap restored DEK");
|
&restored_kek,
|
||||||
|
&meta.wrapped_dek,
|
||||||
|
&meta.header_nonce,
|
||||||
|
&meta.header_tag,
|
||||||
|
)
|
||||||
|
.expect("Unwrap restored DEK");
|
||||||
assert_eq!(*dek, *active_dek);
|
assert_eq!(*dek, *active_dek);
|
||||||
|
|
||||||
let _ = fs::remove_file(&container_path);
|
let _ = fs::remove_file(&container_path);
|
||||||
@@ -427,7 +500,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_restore_from_recovery_key() {
|
fn test_restore_from_recovery_key() {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let container_path: PathBuf = temp_dir.join(format!("test_rec_key_{}.sanctum", std::process::id()));
|
let container_path: PathBuf =
|
||||||
|
temp_dir.join(format!("test_rec_key_{}.sanctum", std::process::id()));
|
||||||
|
|
||||||
if container_path.exists() {
|
if container_path.exists() {
|
||||||
let _ = fs::remove_file(&container_path);
|
let _ = fs::remove_file(&container_path);
|
||||||
@@ -450,7 +524,8 @@ mod tests {
|
|||||||
let phrase = crate::crypto::dek_to_mnemonic(&dek).unwrap();
|
let phrase = crate::crypto::dek_to_mnemonic(&dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&container_path).unwrap();
|
let db = Database::open(&container_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
// Header zerstören
|
// Header zerstören
|
||||||
@@ -466,8 +541,13 @@ mod tests {
|
|||||||
let rescued_db = Database::open(&container_path).unwrap();
|
let rescued_db = Database::open(&container_path).unwrap();
|
||||||
let meta = rescued_db.read_meta().expect("Read rescued meta");
|
let meta = rescued_db.read_meta().expect("Read rescued meta");
|
||||||
let new_kek = derive_kek(new_password, &meta.kdf_salt, &meta.kdf_params).unwrap();
|
let new_kek = derive_kek(new_password, &meta.kdf_salt, &meta.kdf_params).unwrap();
|
||||||
let unwrapped = unwrap_dek(&new_kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag)
|
let unwrapped = unwrap_dek(
|
||||||
.expect("Unwrap rescued DEK");
|
&new_kek,
|
||||||
|
&meta.wrapped_dek,
|
||||||
|
&meta.header_nonce,
|
||||||
|
&meta.header_tag,
|
||||||
|
)
|
||||||
|
.expect("Unwrap rescued DEK");
|
||||||
assert_eq!(*dek, *unwrapped);
|
assert_eq!(*dek, *unwrapped);
|
||||||
|
|
||||||
let _ = fs::remove_file(&container_path);
|
let _ = fs::remove_file(&container_path);
|
||||||
|
|||||||
+352
-194
@@ -9,8 +9,8 @@ use rusqlite::{params, Connection, OptionalExtension};
|
|||||||
|
|
||||||
use crate::crypto::{
|
use crate::crypto::{
|
||||||
decrypt_node_name, derive_kek, encrypt_node_name, generate_dummy_slot, unwrap_key_payload,
|
decrypt_node_name, derive_kek, encrypt_node_name, generate_dummy_slot, unwrap_key_payload,
|
||||||
validate_kdf_params, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1, FORMAT_VERSION_V2,
|
validate_kdf_params, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1,
|
||||||
MAGIC_BYTES,
|
FORMAT_VERSION_V2, MAGIC_BYTES,
|
||||||
};
|
};
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
@@ -52,11 +52,11 @@ pub struct SlotMeta {
|
|||||||
/// garantiert 100%ige Abwärtskompatibilität zu bestehendem Code (z. B. `auth.0`, `auth.2`).
|
/// garantiert 100%ige Abwärtskompatibilität zu bestehendem Code (z. B. `auth.0`, `auth.2`).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UnlockedKeys(
|
pub struct UnlockedKeys(
|
||||||
pub Zeroizing<[u8; 32]>, // 0: DEK (DEK_0 bei Slot 0, DEK_1 bei Slot 1)
|
pub Zeroizing<[u8; 32]>, // 0: DEK (DEK_0 bei Slot 0, DEK_1 bei Slot 1)
|
||||||
pub u32, // 1: Formatversion
|
pub u32, // 1: Formatversion
|
||||||
pub u32, // 2: Slot-ID (0 = Decoy/Standard, 1 = Hidden Vault)
|
pub u32, // 2: Slot-ID (0 = Decoy/Standard, 1 = Hidden Vault)
|
||||||
pub Option<Zeroizing<[u8; 32]>>, // 3: Carrier DEK_0 (bei Slot 1 im Modell A vorhanden)
|
pub Option<Zeroizing<[u8; 32]>>, // 3: Carrier DEK_0 (bei Slot 1 im Modell A vorhanden)
|
||||||
pub Option<i64>, // 4: Carrier Node ID (Inode der Alibi-Datei in nodes)
|
pub Option<i64>, // 4: Carrier Node ID (Inode der Alibi-Datei in nodes)
|
||||||
);
|
);
|
||||||
|
|
||||||
impl UnlockedKeys {
|
impl UnlockedKeys {
|
||||||
@@ -94,12 +94,39 @@ impl ContainerMeta {
|
|||||||
/// Führt für ausnahmslos ALLE vorhandenen Slots die KDF-Ableitung und das DEK-Unwrapping durch.
|
/// Führt für ausnahmslos ALLE vorhandenen Slots die KDF-Ableitung und das DEK-Unwrapping durch.
|
||||||
/// Dadurch ist die Rechenzeit für Decoy und Hidden Vault bit-genau identisch (2x Argon2id).
|
/// Dadurch ist die Rechenzeit für Decoy und Hidden Vault bit-genau identisch (2x Argon2id).
|
||||||
pub fn authenticate(&self, password: &str) -> Option<UnlockedKeys> {
|
pub fn authenticate(&self, password: &str) -> Option<UnlockedKeys> {
|
||||||
|
// SA-01 (HIGH): Strikte strukturelle Vorab-Validierung VOR jeglicher KDF-Berechnung (derive_kek)
|
||||||
|
// Verhindert KDF-Amplification und DoS durch manipulierte Container-Metadaten
|
||||||
|
if self.slots.is_empty() || self.slots.len() > 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !self.slots.iter().any(|s| s.slot_id == 0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self.slots.iter().any(|s| s.slot_id > 1) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut seen_ids = std::collections::HashSet::new();
|
||||||
|
for slot in &self.slots {
|
||||||
|
if !seen_ids.insert(slot.slot_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if validate_kdf_params(&slot.kdf_params).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut matching = None;
|
let mut matching = None;
|
||||||
for slot in &self.slots {
|
for slot in &self.slots {
|
||||||
let res = derive_kek(password, &slot.kdf_salt, &slot.kdf_params)
|
let res = derive_kek(password, &slot.kdf_salt, &slot.kdf_params)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|kek| {
|
.and_then(|kek| {
|
||||||
unwrap_key_payload(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag).ok()
|
unwrap_key_payload(
|
||||||
|
&kek,
|
||||||
|
&slot.wrapped_dek,
|
||||||
|
&slot.header_nonce,
|
||||||
|
&slot.header_tag,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(payload) = res {
|
if let Some(payload) = res {
|
||||||
@@ -109,7 +136,11 @@ impl ContainerMeta {
|
|||||||
let carrier_node_id = if payload.len() >= 40 {
|
let carrier_node_id = if payload.len() >= 40 {
|
||||||
dek.copy_from_slice(&payload[0..32]);
|
dek.copy_from_slice(&payload[0..32]);
|
||||||
let cid = i64::from_le_bytes(payload[32..40].try_into().unwrap());
|
let cid = i64::from_le_bytes(payload[32..40].try_into().unwrap());
|
||||||
if cid > 0 { Some(cid) } else { None }
|
if cid > 0 {
|
||||||
|
Some(cid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} else if payload.len() >= 32 {
|
} else if payload.len() >= 32 {
|
||||||
dek.copy_from_slice(&payload[0..32]);
|
dek.copy_from_slice(&payload[0..32]);
|
||||||
None
|
None
|
||||||
@@ -135,7 +166,13 @@ impl ContainerMeta {
|
|||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
matching = Some(UnlockedKeys(dek_1, slot.version, 1, carrier_dek, carrier_node_id));
|
matching = Some(UnlockedKeys(
|
||||||
|
dek_1,
|
||||||
|
slot.version,
|
||||||
|
1,
|
||||||
|
carrier_dek,
|
||||||
|
carrier_node_id,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,6 +279,27 @@ impl Database {
|
|||||||
path_ref.display()
|
path_ref.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SA-01 (HIGH): Sofortige strukturelle Validierung der Slot-Anzahl bei Database::open (Schutz vor KDF-Amplification/DoS)
|
||||||
|
let has_meta_table: bool = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='meta' LIMIT 1;",
|
||||||
|
[],
|
||||||
|
|_| Ok(()),
|
||||||
|
)
|
||||||
|
.optional()?
|
||||||
|
.is_some();
|
||||||
|
if has_meta_table {
|
||||||
|
let slot_count: i64 = conn
|
||||||
|
.query_row("SELECT count(*) FROM meta;", [], |r| r.get(0))
|
||||||
|
.unwrap_or(0);
|
||||||
|
if slot_count > 2 {
|
||||||
|
bail!(
|
||||||
|
"Ungültige Slot-Anzahl im Container: {} (maximal 2 erlaubt)",
|
||||||
|
slot_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = Self {
|
let db = Self {
|
||||||
@@ -285,17 +343,25 @@ impl Database {
|
|||||||
/// Führt automatische, rückwärtskompatible Schema-Upgrades (z. B. Spalte slot_id, auto_vacuum, is_carrier) durch.
|
/// Führt automatische, rückwärtskompatible Schema-Upgrades (z. B. Spalte slot_id, auto_vacuum, is_carrier) durch.
|
||||||
pub fn ensure_schema_upgrades(&self) -> Result<()> {
|
pub fn ensure_schema_upgrades(&self) -> Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let av: i64 = conn.query_row("PRAGMA auto_vacuum;", [], |r| r.get(0)).unwrap_or(0);
|
let av: i64 = conn
|
||||||
|
.query_row("PRAGMA auto_vacuum;", [], |r| r.get(0))
|
||||||
|
.unwrap_or(0);
|
||||||
if av != 2 {
|
if av != 2 {
|
||||||
// Upgrade bestehender Datenbanken auf INCREMENTAL auto_vacuum
|
// Upgrade bestehender Datenbanken auf INCREMENTAL auto_vacuum
|
||||||
let _ = conn.execute_batch("PRAGMA auto_vacuum = INCREMENTAL; VACUUM;");
|
let _ = conn.execute_batch("PRAGMA auto_vacuum = INCREMENTAL; VACUUM;");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spalte slot_id in meta (falls aus v1 migriert)
|
// Spalte slot_id in meta (falls aus v1 migriert)
|
||||||
let _ = conn.execute("ALTER TABLE meta ADD COLUMN slot_id INTEGER NOT NULL DEFAULT 0", []);
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE meta ADD COLUMN slot_id INTEGER NOT NULL DEFAULT 0",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// Spalte is_carrier in nodes (S-03 Carrier-Schutz)
|
// Spalte is_carrier in nodes (S-03 Carrier-Schutz)
|
||||||
let _ = conn.execute("ALTER TABLE nodes ADD COLUMN is_carrier INTEGER NOT NULL DEFAULT 0", []);
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE nodes ADD COLUMN is_carrier INTEGER NOT NULL DEFAULT 0",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -318,7 +384,9 @@ impl Database {
|
|||||||
/// Führt ein inkrementelles Auto-Vacuum aus, um freigegebene Datenbankseiten an das Betriebssystem zurückzugeben.
|
/// Führt ein inkrementelles Auto-Vacuum aus, um freigegebene Datenbankseiten an das Betriebssystem zurückzugeben.
|
||||||
pub fn incremental_vacuum(&self, pages: Option<usize>) -> Result<usize> {
|
pub fn incremental_vacuum(&self, pages: Option<usize>) -> Result<usize> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let before: i64 = conn.query_row("PRAGMA freelist_count;", [], |r| r.get(0)).unwrap_or(0);
|
let before: i64 = conn
|
||||||
|
.query_row("PRAGMA freelist_count;", [], |r| r.get(0))
|
||||||
|
.unwrap_or(0);
|
||||||
if before <= 0 {
|
if before <= 0 {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
@@ -337,7 +405,9 @@ impl Database {
|
|||||||
drop(rows);
|
drop(rows);
|
||||||
drop(stmt);
|
drop(stmt);
|
||||||
|
|
||||||
let after: i64 = conn.query_row("PRAGMA freelist_count;", [], |r| r.get(0)).unwrap_or(0);
|
let after: i64 = conn
|
||||||
|
.query_row("PRAGMA freelist_count;", [], |r| r.get(0))
|
||||||
|
.unwrap_or(0);
|
||||||
let actual_freed = (before - after).max(0) as usize;
|
let actual_freed = (before - after).max(0) as usize;
|
||||||
Ok(actual_freed.max(stepped))
|
Ok(actual_freed.max(stepped))
|
||||||
}
|
}
|
||||||
@@ -352,9 +422,7 @@ impl Database {
|
|||||||
/// Sucht nach einem existierenden Carrier-Knoten (is_carrier = 1) (S-03).
|
/// Sucht nach einem existierenden Carrier-Knoten (is_carrier = 1) (S-03).
|
||||||
pub fn find_carrier_node_id(&self) -> Result<Option<i64>> {
|
pub fn find_carrier_node_id(&self) -> Result<Option<i64>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare("SELECT id FROM nodes WHERE is_carrier = 1 LIMIT 1")?;
|
||||||
"SELECT id FROM nodes WHERE is_carrier = 1 LIMIT 1",
|
|
||||||
)?;
|
|
||||||
let id = stmt.query_row([], |r| r.get::<_, i64>(0)).optional()?;
|
let id = stmt.query_row([], |r| r.get::<_, i64>(0)).optional()?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
@@ -380,16 +448,26 @@ impl Database {
|
|||||||
)
|
)
|
||||||
SELECT 1 FROM sub WHERE id = ?2 LIMIT 1;",
|
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()?;
|
let exists: Option<i64> = stmt
|
||||||
|
.query_row(params![ancestor_id, node_id], |r| r.get(0))
|
||||||
|
.optional()?;
|
||||||
Ok(exists.is_some())
|
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).
|
||||||
|
///
|
||||||
|
/// SA-07 / Technischer Hinweis zu sicherem Löschen:
|
||||||
|
/// Diese Funktion führt eine *logische* Datenbereinigung durch (Überschreiben aller SQLite-Records
|
||||||
|
/// des Knotens mit CSPRNG-Zufallsrauschen vor dem Löschen). Sie schützt zuverlässig vor
|
||||||
|
/// logischer Wiederherstellung auf Dateisystem- und Datenbankebene.
|
||||||
|
/// Auf modernen Solid-State-Drives (SSD, NVMe) oder Copy-on-Write-Dateisystemen (Btrfs, ZFS, APFS, ReFS)
|
||||||
|
/// kann hierdurch jedoch konstruktionsbedingt keine *physikalische* Datenträgerbereinigung (Media Sanitization)
|
||||||
|
/// garantiert werden, da der Flash Translation Layer (FTL) und Wear-Leveling-Mechanismen Sektoren
|
||||||
|
/// neuen Flash-Speicherzellen zuweisen.
|
||||||
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();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt =
|
||||||
"SELECT chunk_index, length(ciphertext) FROM chunks WHERE node_id = ?1"
|
conn.prepare("SELECT chunk_index, length(ciphertext) FROM chunks WHERE node_id = ?1")?;
|
||||||
)?;
|
|
||||||
let chunks: Vec<(u32, usize)> = stmt
|
let chunks: Vec<(u32, usize)> = stmt
|
||||||
.query_map(params![node_id], |row| Ok((row.get(0)?, row.get(1)?)))?
|
.query_map(params![node_id], |row| Ok((row.get(0)?, row.get(1)?)))?
|
||||||
.filter_map(|r| r.ok())
|
.filter_map(|r| r.ok())
|
||||||
@@ -426,15 +504,15 @@ impl Database {
|
|||||||
header_nonce_0: &[u8; 12],
|
header_nonce_0: &[u8; 12],
|
||||||
header_tag_0: &[u8; 16],
|
header_tag_0: &[u8; 16],
|
||||||
carrier_config: Option<(
|
carrier_config: Option<(
|
||||||
&str, // carrier_name
|
&str, // carrier_name
|
||||||
u64, // carrier_size_bytes
|
u64, // carrier_size_bytes
|
||||||
&[u8; 16], // salt_1
|
&[u8; 16], // salt_1
|
||||||
&KdfParams, // kdf_params_1
|
&KdfParams, // kdf_params_1
|
||||||
&[u8], // wrapped_dek_1 (72B)
|
&[u8], // wrapped_dek_1 (72B)
|
||||||
&[u8; 12], // header_nonce_1
|
&[u8; 12], // header_nonce_1
|
||||||
&[u8; 16], // header_tag_1
|
&[u8; 16], // header_tag_1
|
||||||
&[u8; 32], // raw DEK_0
|
&[u8; 32], // raw DEK_0
|
||||||
&[u8; 32], // raw DEK_1
|
&[u8; 32], // raw DEK_1
|
||||||
)>,
|
)>,
|
||||||
) -> Result<Option<i64>> {
|
) -> Result<Option<i64>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
@@ -547,13 +625,8 @@ impl Database {
|
|||||||
let manifest = crate::carrier::CarrierManifest::new(total_blocks);
|
let manifest = crate::carrier::CarrierManifest::new(total_blocks);
|
||||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||||
|
|
||||||
let (inner_ct, inner_nonce, inner_tag) = crate::crypto::encrypt_chunk(
|
let (inner_ct, inner_nonce, inner_tag) =
|
||||||
dek_1,
|
crate::crypto::encrypt_chunk(dek_1, c_id, 0, &manifest_bytes, FORMAT_VERSION)?;
|
||||||
c_id,
|
|
||||||
0,
|
|
||||||
&manifest_bytes,
|
|
||||||
FORMAT_VERSION,
|
|
||||||
)?;
|
|
||||||
let inner_ct_len = inner_ct.len() as u32;
|
let inner_ct_len = inner_ct.len() as u32;
|
||||||
|
|
||||||
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
||||||
@@ -568,13 +641,8 @@ impl Database {
|
|||||||
}
|
}
|
||||||
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
||||||
|
|
||||||
let (outer_ct, outer_nonce, outer_tag) = crate::crypto::encrypt_chunk(
|
let (outer_ct, outer_nonce, outer_tag) =
|
||||||
dek_0,
|
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext, FORMAT_VERSION)?;
|
||||||
c_id,
|
|
||||||
0,
|
|
||||||
&outer_plaintext,
|
|
||||||
FORMAT_VERSION,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||||
@@ -593,20 +661,9 @@ impl Database {
|
|||||||
|
|
||||||
conn.execute_batch("BEGIN TRANSACTION;")?;
|
conn.execute_batch("BEGIN TRANSACTION;")?;
|
||||||
for b in 1..total_blocks {
|
for b in 1..total_blocks {
|
||||||
let (ct, nonce, tag) = crate::crypto::encrypt_chunk(
|
let (ct, nonce, tag) =
|
||||||
dek_0,
|
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION)?;
|
||||||
c_id,
|
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;
|
||||||
b,
|
|
||||||
&dummy_noise,
|
|
||||||
FORMAT_VERSION,
|
|
||||||
)?;
|
|
||||||
chunk_stmt.execute(params![
|
|
||||||
c_id,
|
|
||||||
b,
|
|
||||||
nonce.as_slice(),
|
|
||||||
tag.as_slice(),
|
|
||||||
ct,
|
|
||||||
])?;
|
|
||||||
|
|
||||||
if b % 500 == 0 {
|
if b % 500 == 0 {
|
||||||
conn.execute_batch("COMMIT; BEGIN TRANSACTION;")?;
|
conn.execute_batch("COMMIT; BEGIN TRANSACTION;")?;
|
||||||
@@ -767,18 +824,43 @@ impl Database {
|
|||||||
header_nonce: &[u8; 12],
|
header_nonce: &[u8; 12],
|
||||||
header_tag: &[u8; 16],
|
header_tag: &[u8; 16],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.init_schema_with_hidden(salt, kdf_params, wrapped_dek, header_nonce, header_tag, None)
|
self.init_schema_with_hidden(
|
||||||
|
salt,
|
||||||
|
kdf_params,
|
||||||
|
wrapped_dek,
|
||||||
|
header_nonce,
|
||||||
|
header_tag,
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Liest alle Header-Slots aus der `meta`-Tabelle aus (Slot 0 = Decoy/Standard, Slot 1 = Hidden Vault oder Dummy-Rauschen).
|
/// Liest alle Header-Slots aus der `meta`-Tabelle aus (Slot 0 = Decoy/Standard, Slot 1 = Hidden Vault oder Dummy-Rauschen).
|
||||||
|
/// SA-01: Führt strikte Vorab-Validierung der Container-Struktur VOR jeglicher KDF-Berechnung durch.
|
||||||
pub fn read_slots(&self) -> Result<Vec<SlotMeta>> {
|
pub fn read_slots(&self) -> Result<Vec<SlotMeta>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
|
||||||
|
// SA-01 (HIGH): Vorab-Prüfung der Gesamtzahl der Slots (Schutz gegen KDF-Amplification / Container DoS)
|
||||||
|
let slot_count: i64 = conn.query_row("SELECT count(*) FROM meta", [], |r| r.get(0))?;
|
||||||
|
if slot_count == 0 {
|
||||||
|
bail!("Container-Header ist leer oder beschädigt");
|
||||||
|
}
|
||||||
|
if slot_count > 2 {
|
||||||
|
bail!(
|
||||||
|
"Ungültige Slot-Anzahl im Container: {} (maximal 2 erlaubt)",
|
||||||
|
slot_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT slot_id, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag
|
"SELECT slot_id, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag
|
||||||
FROM meta ORDER BY slot_id ASC",
|
FROM meta ORDER BY slot_id ASC",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let rows = stmt.query_map([], |row| {
|
let mut rows = stmt.query([])?;
|
||||||
|
let mut slots = Vec::new();
|
||||||
|
let mut seen_slot_ids = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
let slot_id: u32 = row.get(0)?;
|
let slot_id: u32 = row.get(0)?;
|
||||||
let version: u32 = row.get(1)?;
|
let version: u32 = row.get(1)?;
|
||||||
let salt_vec: Vec<u8> = row.get(2)?;
|
let salt_vec: Vec<u8> = row.get(2)?;
|
||||||
@@ -787,91 +869,69 @@ impl Database {
|
|||||||
let nonce_vec: Vec<u8> = row.get(5)?;
|
let nonce_vec: Vec<u8> = row.get(5)?;
|
||||||
let tag_vec: Vec<u8> = row.get(6)?;
|
let tag_vec: Vec<u8> = row.get(6)?;
|
||||||
|
|
||||||
let mut kdf_salt = [0u8; 16];
|
// SA-01: Ausschließlich die Slot-IDs 0 und 1 sind zulässig
|
||||||
if salt_vec.len() != 16 {
|
if slot_id > 1 {
|
||||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
bail!(
|
||||||
2,
|
"Ungültige Slot-ID {}: Es sind ausschließlich die Slot-IDs 0 und 1 erlaubt",
|
||||||
rusqlite::types::Type::Blob,
|
slot_id
|
||||||
Box::new(std::io::Error::new(
|
);
|
||||||
std::io::ErrorKind::InvalidData,
|
|
||||||
format!("Ungültige Salt-Länge in Slot {}: {} Bytes (erwartet: 16)", slot_id, salt_vec.len()),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
if !seen_slot_ids.insert(slot_id) {
|
||||||
|
bail!("Doppelte Slot-ID {} im Container-Header entdeckt", slot_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if salt_vec.len() != 16 {
|
||||||
|
bail!(
|
||||||
|
"Ungültige Salt-Länge in Slot {}: {} Bytes (erwartet: 16)",
|
||||||
|
slot_id,
|
||||||
|
salt_vec.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut kdf_salt = [0u8; 16];
|
||||||
kdf_salt.copy_from_slice(&salt_vec);
|
kdf_salt.copy_from_slice(&salt_vec);
|
||||||
|
|
||||||
let mut header_nonce = [0u8; 12];
|
|
||||||
if nonce_vec.len() != 12 {
|
if nonce_vec.len() != 12 {
|
||||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
bail!(
|
||||||
5,
|
"Ungültige Nonce-Länge in Slot {}: {} Bytes (erwartet: 12)",
|
||||||
rusqlite::types::Type::Blob,
|
slot_id,
|
||||||
Box::new(std::io::Error::new(
|
nonce_vec.len()
|
||||||
std::io::ErrorKind::InvalidData,
|
);
|
||||||
format!("Ungültige Nonce-Länge in Slot {}: {} Bytes (erwartet: 12)", slot_id, nonce_vec.len()),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
let mut header_nonce = [0u8; 12];
|
||||||
header_nonce.copy_from_slice(&nonce_vec);
|
header_nonce.copy_from_slice(&nonce_vec);
|
||||||
|
|
||||||
let mut header_tag = [0u8; 16];
|
|
||||||
if tag_vec.len() != 16 {
|
if tag_vec.len() != 16 {
|
||||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
bail!(
|
||||||
6,
|
"Ungültige Tag-Länge in Slot {}: {} Bytes (erwartet: 16)",
|
||||||
rusqlite::types::Type::Blob,
|
slot_id,
|
||||||
Box::new(std::io::Error::new(
|
tag_vec.len()
|
||||||
std::io::ErrorKind::InvalidData,
|
);
|
||||||
format!("Ungültige Tag-Länge in Slot {}: {} Bytes (erwartet: 16)", slot_id, tag_vec.len()),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
let mut header_tag = [0u8; 16];
|
||||||
header_tag.copy_from_slice(&tag_vec);
|
header_tag.copy_from_slice(&tag_vec);
|
||||||
|
|
||||||
// Strikte Validierung der wrapped_dek Länge (DoS- und Manipulationsschutz)
|
// Strikte Validierung der wrapped_dek Länge (DoS- und Manipulationsschutz)
|
||||||
if slot_id == 0 {
|
if slot_id == 0 {
|
||||||
if wrapped_dek.len() != 40 && wrapped_dek.len() != 32 {
|
if wrapped_dek.len() != 40 && wrapped_dek.len() != 32 {
|
||||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
bail!(
|
||||||
4,
|
"Ungültige wrapped_dek-Länge in Slot 0: {} Bytes (erwartet: 40 oder 32)",
|
||||||
rusqlite::types::Type::Blob,
|
wrapped_dek.len()
|
||||||
Box::new(std::io::Error::new(
|
);
|
||||||
std::io::ErrorKind::InvalidData,
|
|
||||||
format!("Ungültige wrapped_dek-Länge in Slot 0: {} Bytes (erwartet: 40 oder 32)", wrapped_dek.len()),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else if slot_id == 1 {
|
} else if slot_id == 1 {
|
||||||
if wrapped_dek.len() != 72 && wrapped_dek.len() != 64 && wrapped_dek.len() != 32 {
|
if wrapped_dek.len() != 72 && wrapped_dek.len() != 64 && wrapped_dek.len() != 32 {
|
||||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
bail!("Ungültige wrapped_dek-Länge in Slot 1: {} Bytes (erwartet: 72, 64 oder 32)", wrapped_dek.len());
|
||||||
4,
|
|
||||||
rusqlite::types::Type::Blob,
|
|
||||||
Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidData,
|
|
||||||
format!("Ungültige wrapped_dek-Länge in Slot 1: {} Bytes (erwartet: 72, 64 oder 32)", wrapped_dek.len()),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let kdf_params: KdfParams = serde_json::from_str(¶ms_str)
|
let kdf_params: KdfParams = serde_json::from_str(¶ms_str).map_err(|e| {
|
||||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
anyhow::anyhow!("Ungültiges KdfParams-JSON in Slot {}: {e}", slot_id)
|
||||||
3,
|
})?;
|
||||||
rusqlite::types::Type::Text,
|
|
||||||
Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidData,
|
|
||||||
format!("Ungültiges KdfParams-JSON in Slot {}: {e}", slot_id),
|
|
||||||
)),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
validate_kdf_params(&kdf_params)
|
validate_kdf_params(&kdf_params)
|
||||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
.map_err(|e| anyhow::anyhow!("KDF-Parameter in Slot {} ungültig: {e}", slot_id))?;
|
||||||
3,
|
|
||||||
rusqlite::types::Type::Text,
|
|
||||||
Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidData,
|
|
||||||
format!("KDF-Parameter in Slot {} ungültig: {e}", slot_id),
|
|
||||||
)),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(SlotMeta {
|
slots.push(SlotMeta {
|
||||||
slot_id,
|
slot_id,
|
||||||
version,
|
version,
|
||||||
kdf_salt,
|
kdf_salt,
|
||||||
@@ -879,13 +939,14 @@ impl Database {
|
|||||||
wrapped_dek,
|
wrapped_dek,
|
||||||
header_nonce,
|
header_nonce,
|
||||||
header_tag,
|
header_tag,
|
||||||
})
|
});
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut slots = Vec::new();
|
|
||||||
for r in rows {
|
|
||||||
slots.push(r?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SA-01: Slot 0 (Standard/Decoy Vault) ist zwingend erforderlich
|
||||||
|
if !seen_slot_ids.contains(&0) {
|
||||||
|
bail!("Ungültiger Container-Header: Slot 0 (Standard/Decoy Vault) fehlt");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(slots)
|
Ok(slots)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,7 +970,10 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if slot0.version != FORMAT_VERSION_V1 && slot0.version != FORMAT_VERSION_V2 {
|
if slot0.version != FORMAT_VERSION_V1 && slot0.version != FORMAT_VERSION_V2 {
|
||||||
bail!("Nicht unterstützte Sanctum-Formatversion: {}", slot0.version);
|
bail!(
|
||||||
|
"Nicht unterstützte Sanctum-Formatversion: {}",
|
||||||
|
slot0.version
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ContainerMeta {
|
Ok(ContainerMeta {
|
||||||
@@ -932,7 +996,14 @@ impl Database {
|
|||||||
new_header_nonce: &[u8; 12],
|
new_header_nonce: &[u8; 12],
|
||||||
new_header_tag: &[u8; 16],
|
new_header_tag: &[u8; 16],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.update_slot_keys(0, new_salt, new_params, new_wrapped_dek, new_header_nonce, new_header_tag)
|
self.update_slot_keys(
|
||||||
|
0,
|
||||||
|
new_salt,
|
||||||
|
new_params,
|
||||||
|
new_wrapped_dek,
|
||||||
|
new_header_nonce,
|
||||||
|
new_header_tag,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aktualisiert die kryptografischen Schlüssel eines bestimmten Slots.
|
/// Aktualisiert die kryptografischen Schlüssel eines bestimmten Slots.
|
||||||
@@ -960,7 +1031,10 @@ impl Database {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
if rows_affected == 0 {
|
if rows_affected == 0 {
|
||||||
bail!("Konnte Container-Header für Slot {} nicht aktualisieren", slot_id);
|
bail!(
|
||||||
|
"Konnte Container-Header für Slot {} nicht aktualisieren",
|
||||||
|
slot_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1059,7 +1133,8 @@ impl Database {
|
|||||||
let mut matched_record = None;
|
let mut matched_record = None;
|
||||||
for r in rows {
|
for r in rows {
|
||||||
let (id, p_id, enc_name, is_dir, size, c_at, m_at) = r?;
|
let (id, p_id, enc_name, is_dir, size, c_at, m_at) = r?;
|
||||||
let dec_name = decrypt_node_name(dek, p_id.unwrap_or(0), &enc_name).unwrap_or(enc_name);
|
let dec_name =
|
||||||
|
decrypt_node_name(dek, p_id.unwrap_or(0), &enc_name).unwrap_or(enc_name);
|
||||||
if dec_name == *segment {
|
if dec_name == *segment {
|
||||||
matched_record = Some(NodeRecord {
|
matched_record = Some(NodeRecord {
|
||||||
id,
|
id,
|
||||||
@@ -1471,11 +1546,10 @@ impl Database {
|
|||||||
/// 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.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let _res: (i64, i64, i64) = conn.query_row(
|
let _res: (i64, i64, i64) =
|
||||||
"PRAGMA wal_checkpoint(TRUNCATE);",
|
conn.query_row("PRAGMA wal_checkpoint(TRUNCATE);", [], |row| {
|
||||||
[],
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
||||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
})?;
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1650,10 +1724,9 @@ impl Database {
|
|||||||
/// Zählt die Anzahl von Verzeichnissen, Dateien und Daten-Chunks im Container.
|
/// Zählt die Anzahl von Verzeichnissen, Dateien und Daten-Chunks im Container.
|
||||||
pub fn count_nodes_and_chunks(&self) -> Result<(usize, usize, usize)> {
|
pub fn count_nodes_and_chunks(&self) -> Result<(usize, usize, usize)> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let dirs: i64 =
|
let dirs: i64 = conn.query_row("SELECT COUNT(*) FROM nodes WHERE is_dir = 1", [], |r| {
|
||||||
conn.query_row("SELECT COUNT(*) FROM nodes WHERE is_dir = 1", [], |r| {
|
r.get(0)
|
||||||
r.get(0)
|
})?;
|
||||||
})?;
|
|
||||||
let files: i64 =
|
let files: i64 =
|
||||||
conn.query_row("SELECT COUNT(*) FROM nodes WHERE is_dir = 0", [], |r| {
|
conn.query_row("SELECT COUNT(*) FROM nodes WHERE is_dir = 0", [], |r| {
|
||||||
r.get(0)
|
r.get(0)
|
||||||
@@ -1717,7 +1790,8 @@ mod tests {
|
|||||||
let nonce = [3u8; 12];
|
let nonce = [3u8; 12];
|
||||||
let tag = [4u8; 16];
|
let tag = [4u8; 16];
|
||||||
|
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Meta abrufen
|
// Meta abrufen
|
||||||
let meta = db.read_meta().unwrap();
|
let meta = db.read_meta().unwrap();
|
||||||
@@ -1740,19 +1814,29 @@ mod tests {
|
|||||||
assert!(!file.is_dir);
|
assert!(!file.is_dir);
|
||||||
|
|
||||||
// Pfadauflösung testen
|
// Pfadauflösung testen
|
||||||
let resolved_file = db.resolve_path("/documents/notes.txt").unwrap().expect("File should resolve");
|
let resolved_file = db
|
||||||
|
.resolve_path("/documents/notes.txt")
|
||||||
|
.unwrap()
|
||||||
|
.expect("File should resolve");
|
||||||
assert_eq!(resolved_file.id, file.id);
|
assert_eq!(resolved_file.id, file.id);
|
||||||
|
|
||||||
let resolved_docs = db.resolve_path("documents").unwrap().expect("Docs should resolve");
|
let resolved_docs = db
|
||||||
|
.resolve_path("documents")
|
||||||
|
.unwrap()
|
||||||
|
.expect("Docs should resolve");
|
||||||
assert_eq!(resolved_docs.id, docs.id);
|
assert_eq!(resolved_docs.id, docs.id);
|
||||||
|
|
||||||
// Chunks schreiben & lesen
|
// Chunks schreiben & lesen
|
||||||
let test_cipher = b"ENCRYPTED_DATA_BLOCK";
|
let test_cipher = b"ENCRYPTED_DATA_BLOCK";
|
||||||
let c_nonce = [7u8; 12];
|
let c_nonce = [7u8; 12];
|
||||||
let c_tag = [8u8; 16];
|
let c_tag = [8u8; 16];
|
||||||
db.write_chunk(file.id, 0, &c_nonce, &c_tag, test_cipher).unwrap();
|
db.write_chunk(file.id, 0, &c_nonce, &c_tag, test_cipher)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let chunk = db.read_chunk(file.id, 0).unwrap().expect("Chunk 0 should exist");
|
let chunk = db
|
||||||
|
.read_chunk(file.id, 0)
|
||||||
|
.unwrap()
|
||||||
|
.expect("Chunk 0 should exist");
|
||||||
assert_eq!(chunk.ciphertext, test_cipher);
|
assert_eq!(chunk.ciphertext, test_cipher);
|
||||||
|
|
||||||
// Truncate
|
// Truncate
|
||||||
@@ -1782,7 +1866,8 @@ mod tests {
|
|||||||
let nonce = [3u8; 12];
|
let nonce = [3u8; 12];
|
||||||
let tag = [4u8; 16];
|
let tag = [4u8; 16];
|
||||||
|
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let root = db.resolve_path("/").unwrap().expect("Root node");
|
let root = db.resolve_path("/").unwrap().expect("Root node");
|
||||||
let file = db.create_node(root.id, "large_file.bin", false).unwrap();
|
let file = db.create_node(root.id, "large_file.bin", false).unwrap();
|
||||||
@@ -1792,7 +1877,8 @@ mod tests {
|
|||||||
let c_nonce = [5u8; 12];
|
let c_nonce = [5u8; 12];
|
||||||
let c_tag = [6u8; 16];
|
let c_tag = [6u8; 16];
|
||||||
for i in 0..20 {
|
for i in 0..20 {
|
||||||
db.write_chunk(file.id, i, &c_nonce, &c_tag, &payload).unwrap();
|
db.write_chunk(file.id, i, &c_nonce, &c_tag, &payload)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
@@ -1801,12 +1887,18 @@ mod tests {
|
|||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
let freelist_before = db.freelist_count().unwrap();
|
let freelist_before = db.freelist_count().unwrap();
|
||||||
assert!(freelist_before > 0, "Freelist sollte nach dem Löschen freie Seiten enthalten");
|
assert!(
|
||||||
|
freelist_before > 0,
|
||||||
|
"Freelist sollte nach dem Löschen freie Seiten enthalten"
|
||||||
|
);
|
||||||
|
|
||||||
// Incremental Vacuum ausführen
|
// Incremental Vacuum ausführen
|
||||||
let freed = db.incremental_vacuum(None).unwrap();
|
let freed = db.incremental_vacuum(None).unwrap();
|
||||||
assert!(freed > 0, "Es sollten Seiten freigegeben werden");
|
assert!(freed > 0, "Es sollten Seiten freigegeben werden");
|
||||||
assert_eq!(freed, freelist_before, "Alle freien Seiten müssen freigegeben werden");
|
assert_eq!(
|
||||||
|
freed, freelist_before,
|
||||||
|
"Alle freien Seiten müssen freigegeben werden"
|
||||||
|
);
|
||||||
|
|
||||||
let freelist_after = db.freelist_count().unwrap();
|
let freelist_after = db.freelist_count().unwrap();
|
||||||
assert_eq!(freelist_after, 0, "Freelist sollte nach Vacuum 0 sein");
|
assert_eq!(freelist_after, 0, "Freelist sollte nach Vacuum 0 sein");
|
||||||
@@ -1823,22 +1915,34 @@ mod tests {
|
|||||||
let nonce = [3u8; 12];
|
let nonce = [3u8; 12];
|
||||||
let tag = [4u8; 16];
|
let tag = [4u8; 16];
|
||||||
|
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
let root = db.resolve_path("/").unwrap().expect("Root");
|
let root = db.resolve_path("/").unwrap().expect("Root");
|
||||||
let file = db.create_node(root.id, "sensitive.dat", false).unwrap();
|
let file = db.create_node(root.id, "sensitive.dat", false).unwrap();
|
||||||
|
|
||||||
let sensitive_payload = b"VERY_SENSITIVE_PLAINTEXT_OR_CIPHERTEXT";
|
let sensitive_payload = b"VERY_SENSITIVE_PLAINTEXT_OR_CIPHERTEXT";
|
||||||
let c_nonce = [10u8; 12];
|
let c_nonce = [10u8; 12];
|
||||||
let c_tag = [11u8; 16];
|
let c_tag = [11u8; 16];
|
||||||
db.write_chunk(file.id, 0, &c_nonce, &c_tag, sensitive_payload).unwrap();
|
db.write_chunk(file.id, 0, &c_nonce, &c_tag, sensitive_payload)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Shredde Chunks
|
// Shredde Chunks
|
||||||
db.shred_chunks_for_node(file.id).unwrap();
|
db.shred_chunks_for_node(file.id).unwrap();
|
||||||
|
|
||||||
// Prüfe, was sich in der Chunks-Tabelle befindet
|
// Prüfe, was sich in der Chunks-Tabelle befindet
|
||||||
let chunk = db.read_chunk(file.id, 0).unwrap().expect("Chunk existiert noch");
|
let chunk = db
|
||||||
assert_ne!(chunk.ciphertext, sensitive_payload, "Ciphertext muss überschrieben sein!");
|
.read_chunk(file.id, 0)
|
||||||
assert_eq!(chunk.ciphertext.len(), sensitive_payload.len(), "Länge muss identisch sein");
|
.unwrap()
|
||||||
|
.expect("Chunk existiert noch");
|
||||||
|
assert_ne!(
|
||||||
|
chunk.ciphertext, sensitive_payload,
|
||||||
|
"Ciphertext muss überschrieben sein!"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
chunk.ciphertext.len(),
|
||||||
|
sensitive_payload.len(),
|
||||||
|
"Länge muss identisch sein"
|
||||||
|
);
|
||||||
assert_ne!(chunk.nonce, c_nonce, "Nonce muss überschrieben sein");
|
assert_ne!(chunk.nonce, c_nonce, "Nonce muss überschrieben sein");
|
||||||
assert_ne!(chunk.tag, c_tag, "Tag muss überschrieben sein");
|
assert_ne!(chunk.tag, c_tag, "Tag muss überschrieben sein");
|
||||||
}
|
}
|
||||||
@@ -1862,9 +1966,14 @@ mod tests {
|
|||||||
let dek1 = [0xBBu8; 32];
|
let dek1 = [0xBBu8; 32];
|
||||||
|
|
||||||
db.init_schema_with_hidden(
|
db.init_schema_with_hidden(
|
||||||
&salt0, &kdf_params0, &wrapped_dek0, &nonce0, &tag0,
|
&salt0,
|
||||||
|
&kdf_params0,
|
||||||
|
&wrapped_dek0,
|
||||||
|
&nonce0,
|
||||||
|
&tag0,
|
||||||
Some((&salt1, &kdf_params1, &wrapped_dek1, &nonce1, &tag1)),
|
Some((&salt1, &kdf_params1, &wrapped_dek1, &nonce1, &tag1)),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Slots prüfen
|
// Slots prüfen
|
||||||
let slots = db.read_slots().unwrap();
|
let slots = db.read_slots().unwrap();
|
||||||
@@ -1873,63 +1982,107 @@ mod tests {
|
|||||||
assert_eq!(slots[1].slot_id, 1);
|
assert_eq!(slots[1].slot_id, 1);
|
||||||
|
|
||||||
// Datei in Vault 0 (Decoy) erstellen
|
// Datei in Vault 0 (Decoy) erstellen
|
||||||
let root0 = db.resolve_path_in_vault("/", 0, &dek0).unwrap().expect("Root 0");
|
let root0 = db
|
||||||
|
.resolve_path_in_vault("/", 0, &dek0)
|
||||||
|
.unwrap()
|
||||||
|
.expect("Root 0");
|
||||||
assert_eq!(root0.id, 1);
|
assert_eq!(root0.id, 1);
|
||||||
let decoy_file = db.create_node_in_vault(0, root0.id, "public_recipe.txt", false, &dek0).unwrap();
|
let decoy_file = db
|
||||||
|
.create_node_in_vault(0, root0.id, "public_recipe.txt", false, &dek0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Datei in Vault 1 (Hidden) erstellen
|
// Datei in Vault 1 (Hidden) erstellen
|
||||||
let root1 = db.resolve_path_in_vault("/", 1, &dek1).unwrap().expect("Root 1");
|
let root1 = db
|
||||||
|
.resolve_path_in_vault("/", 1, &dek1)
|
||||||
|
.unwrap()
|
||||||
|
.expect("Root 1");
|
||||||
assert_eq!(root1.id, 2);
|
assert_eq!(root1.id, 2);
|
||||||
let hidden_file = db.create_node_in_vault(1, root1.id, "classified_leak.pdf", false, &dek1).unwrap();
|
let hidden_file = db
|
||||||
|
.create_node_in_vault(1, root1.id, "classified_leak.pdf", false, &dek1)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Auflösen in Vault 0: Sieht nur public_recipe.txt
|
// Auflösen in Vault 0: Sieht nur public_recipe.txt
|
||||||
let res_decoy = db.resolve_path_in_vault("/public_recipe.txt", 0, &dek0).unwrap();
|
let res_decoy = db
|
||||||
|
.resolve_path_in_vault("/public_recipe.txt", 0, &dek0)
|
||||||
|
.unwrap();
|
||||||
assert!(res_decoy.is_some());
|
assert!(res_decoy.is_some());
|
||||||
assert_eq!(res_decoy.unwrap().id, decoy_file.id);
|
assert_eq!(res_decoy.unwrap().id, decoy_file.id);
|
||||||
|
|
||||||
let res_hidden_in_v0 = db.resolve_path_in_vault("/classified_leak.pdf", 0, &dek0).unwrap();
|
let res_hidden_in_v0 = db
|
||||||
assert!(res_hidden_in_v0.is_none(), "Vault 0 darf keine Dateien aus Hidden Vault auflösen!");
|
.resolve_path_in_vault("/classified_leak.pdf", 0, &dek0)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
res_hidden_in_v0.is_none(),
|
||||||
|
"Vault 0 darf keine Dateien aus Hidden Vault auflösen!"
|
||||||
|
);
|
||||||
|
|
||||||
// Auflösen in Vault 1: Sieht nur classified_leak.pdf
|
// Auflösen in Vault 1: Sieht nur classified_leak.pdf
|
||||||
let res_hidden = db.resolve_path_in_vault("/classified_leak.pdf", 1, &dek1).unwrap();
|
let res_hidden = db
|
||||||
|
.resolve_path_in_vault("/classified_leak.pdf", 1, &dek1)
|
||||||
|
.unwrap();
|
||||||
assert!(res_hidden.is_some());
|
assert!(res_hidden.is_some());
|
||||||
assert_eq!(res_hidden.unwrap().id, hidden_file.id);
|
assert_eq!(res_hidden.unwrap().id, hidden_file.id);
|
||||||
|
|
||||||
let res_decoy_in_v1 = db.resolve_path_in_vault("/public_recipe.txt", 1, &dek1).unwrap();
|
let res_decoy_in_v1 = db
|
||||||
assert!(res_decoy_in_v1.is_none(), "Vault 1 darf keine Dateien aus Vault 0 auflösen!");
|
.resolve_path_in_vault("/public_recipe.txt", 1, &dek1)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
res_decoy_in_v1.is_none(),
|
||||||
|
"Vault 1 darf keine Dateien aus Vault 0 auflösen!"
|
||||||
|
);
|
||||||
|
|
||||||
// Forensische Prüfung: Roh-Inspektion der SQLite-Tabellen
|
// Forensische Prüfung: Roh-Inspektion der SQLite-Tabellen
|
||||||
let conn = db.conn.lock().unwrap();
|
let conn = db.conn.lock().unwrap();
|
||||||
let raw_name_v0: String = conn.query_row(
|
let raw_name_v0: String = conn
|
||||||
"SELECT name FROM nodes WHERE id = ?1",
|
.query_row(
|
||||||
params![decoy_file.id],
|
"SELECT name FROM nodes WHERE id = ?1",
|
||||||
|r| r.get(0),
|
params![decoy_file.id],
|
||||||
).unwrap();
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(raw_name_v0, "public_recipe.txt");
|
assert_eq!(raw_name_v0, "public_recipe.txt");
|
||||||
|
|
||||||
let raw_name_v1: String = conn.query_row(
|
let raw_name_v1: String = conn
|
||||||
"SELECT name FROM nodes WHERE id = ?1",
|
.query_row(
|
||||||
params![hidden_file.id],
|
"SELECT name FROM nodes WHERE id = ?1",
|
||||||
|r| r.get(0),
|
params![hidden_file.id],
|
||||||
).unwrap();
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
// Dual-Vault Isolation: Kein $h$-Präfix, kein Klartext
|
// Dual-Vault Isolation: Kein $h$-Präfix, kein Klartext
|
||||||
assert!(!raw_name_v1.starts_with("$h$"), "Hidden Vault Dateiname darf kein $h$-Präfix mehr besitzen");
|
assert!(
|
||||||
assert!(!raw_name_v1.contains("classified_leak"), "Plaintext darf keinesfalls in SQLite DB auftauchen");
|
!raw_name_v1.starts_with("$h$"),
|
||||||
|
"Hidden Vault Dateiname darf kein $h$-Präfix mehr besitzen"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!raw_name_v1.contains("classified_leak"),
|
||||||
|
"Plaintext darf keinesfalls in SQLite DB auftauchen"
|
||||||
|
);
|
||||||
|
|
||||||
// Keine Spalte `vault_id` in nodes oder chunks
|
// Keine Spalte `vault_id` in nodes oder chunks
|
||||||
let has_vault_id_nodes: i64 = conn.query_row(
|
let has_vault_id_nodes: i64 = conn
|
||||||
"SELECT count(*) FROM pragma_table_info('nodes') WHERE name = 'vault_id'",
|
.query_row(
|
||||||
[],
|
"SELECT count(*) FROM pragma_table_info('nodes') WHERE name = 'vault_id'",
|
||||||
|r| r.get(0),
|
[],
|
||||||
).unwrap();
|
|r| r.get(0),
|
||||||
assert_eq!(has_vault_id_nodes, 0, "vault_id darf nicht in nodes existieren");
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
has_vault_id_nodes, 0,
|
||||||
|
"vault_id darf nicht in nodes existieren"
|
||||||
|
);
|
||||||
|
|
||||||
let has_vault_id_chunks: i64 = conn.query_row(
|
let has_vault_id_chunks: i64 = conn
|
||||||
"SELECT count(*) FROM pragma_table_info('chunks') WHERE name = 'vault_id'",
|
.query_row(
|
||||||
[],
|
"SELECT count(*) FROM pragma_table_info('chunks') WHERE name = 'vault_id'",
|
||||||
|r| r.get(0),
|
[],
|
||||||
).unwrap();
|
|r| r.get(0),
|
||||||
assert_eq!(has_vault_id_chunks, 0, "vault_id darf nicht in chunks existieren");
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
has_vault_id_chunks, 0,
|
||||||
|
"vault_id darf nicht in chunks existieren"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1942,7 +2095,8 @@ mod tests {
|
|||||||
[3u8; 12],
|
[3u8; 12],
|
||||||
[4u8; 16],
|
[4u8; 16],
|
||||||
);
|
);
|
||||||
db.init_schema(&salt, &kdf, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let file = db.create_node(1, "crash_test.bin", false).unwrap();
|
let file = db.create_node(1, "crash_test.bin", false).unwrap();
|
||||||
assert_eq!(file.size, 0);
|
assert_eq!(file.size, 0);
|
||||||
@@ -1961,10 +2115,14 @@ mod tests {
|
|||||||
&ciphertext,
|
&ciphertext,
|
||||||
new_size,
|
new_size,
|
||||||
modified_at,
|
modified_at,
|
||||||
).expect("Atomic write");
|
)
|
||||||
|
.expect("Atomic write");
|
||||||
|
|
||||||
// Chunk verifizieren
|
// Chunk verifizieren
|
||||||
let chunk = db.read_chunk(file.id, 0).unwrap().expect("Chunk must exist");
|
let chunk = db
|
||||||
|
.read_chunk(file.id, 0)
|
||||||
|
.unwrap()
|
||||||
|
.expect("Chunk must exist");
|
||||||
assert_eq!(chunk.ciphertext, ciphertext);
|
assert_eq!(chunk.ciphertext, ciphertext);
|
||||||
assert_eq!(chunk.nonce, chunk_nonce);
|
assert_eq!(chunk.nonce, chunk_nonce);
|
||||||
assert_eq!(chunk.tag, chunk_tag);
|
assert_eq!(chunk.tag, chunk_tag);
|
||||||
|
|||||||
+159
-36
@@ -141,7 +141,8 @@ pub fn is_excluded(name: &str, rel_path: &str, patterns: &[String]) -> bool {
|
|||||||
// Wildcard-Muster: prefix*
|
// Wildcard-Muster: prefix*
|
||||||
else if p.ends_with('*') && !p[..p.len() - 1].contains('*') {
|
else if p.ends_with('*') && !p[..p.len() - 1].contains('*') {
|
||||||
let prefix = p[..p.len() - 1].to_ascii_lowercase();
|
let prefix = p[..p.len() - 1].to_ascii_lowercase();
|
||||||
if norm_name.starts_with(&prefix) || norm_rel.to_ascii_lowercase().starts_with(&prefix) {
|
if norm_name.starts_with(&prefix) || norm_rel.to_ascii_lowercase().starts_with(&prefix)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +152,9 @@ pub fn is_excluded(name: &str, rel_path: &str, patterns: &[String]) -> bool {
|
|||||||
if norm_name == p_lower {
|
if norm_name == p_lower {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if norm_rel.trim_start_matches('/').to_ascii_lowercase() == p_lower.trim_start_matches('/') {
|
if norm_rel.trim_start_matches('/').to_ascii_lowercase()
|
||||||
|
== p_lower.trim_start_matches('/')
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,8 +179,8 @@ fn read_chunk_buffer(file: &mut File, buf: &mut [u8]) -> std::io::Result<usize>
|
|||||||
|
|
||||||
/// Setzt den Modifikationszeitstempel einer lokalen Datei via std::fs::FileTimes.
|
/// Setzt den Modifikationszeitstempel einer lokalen Datei via std::fs::FileTimes.
|
||||||
fn set_local_file_mtime(file: &File, mtime_secs: u64) {
|
fn set_local_file_mtime(file: &File, mtime_secs: u64) {
|
||||||
let times = std::fs::FileTimes::new()
|
let times =
|
||||||
.set_modified(UNIX_EPOCH + Duration::from_secs(mtime_secs));
|
std::fs::FileTimes::new().set_modified(UNIX_EPOCH + Duration::from_secs(mtime_secs));
|
||||||
let _ = file.set_times(times);
|
let _ = file.set_times(times);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,10 @@ pub fn ensure_vault_dir_tree(
|
|||||||
let children = db.list_children_in_vault(current_id, vault_id, dek)?;
|
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 let Some(existing) = children.into_iter().find(|c| c.name == segment) {
|
||||||
if !existing.is_dir {
|
if !existing.is_dir {
|
||||||
bail!("Pfad-Konflikt: '{}' existiert im Container bereits als Datei", segment);
|
bail!(
|
||||||
|
"Pfad-Konflikt: '{}' existiert im Container bereits als Datei",
|
||||||
|
segment
|
||||||
|
);
|
||||||
}
|
}
|
||||||
current_id = existing.id;
|
current_id = existing.id;
|
||||||
current_node = existing;
|
current_node = existing;
|
||||||
@@ -234,8 +240,12 @@ pub fn sync_single_file_to_vault(
|
|||||||
) -> Result<FileTransferResult> {
|
) -> Result<FileTransferResult> {
|
||||||
validate_node_name(file_name)?;
|
validate_node_name(file_name)?;
|
||||||
|
|
||||||
let meta = fs::metadata(local_path)
|
let meta = fs::metadata(local_path).with_context(|| {
|
||||||
.with_context(|| format!("Konnte Metadaten für '{}' nicht lesen", local_path.display()))?;
|
format!(
|
||||||
|
"Konnte Metadaten für '{}' nicht lesen",
|
||||||
|
local_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let local_size = meta.len();
|
let local_size = meta.len();
|
||||||
let local_mtime = meta
|
let local_mtime = meta
|
||||||
.modified()
|
.modified()
|
||||||
@@ -250,7 +260,10 @@ pub fn sync_single_file_to_vault(
|
|||||||
if let Some(ref node) = existing_node {
|
if let Some(ref node) = existing_node {
|
||||||
db.assert_not_carrier(node.id)?;
|
db.assert_not_carrier(node.id)?;
|
||||||
if node.is_dir {
|
if node.is_dir {
|
||||||
bail!("Pfad-Konflikt: '{}' existiert im Tresor als Ordner", file_name);
|
bail!(
|
||||||
|
"Pfad-Konflikt: '{}' existiert im Tresor als Ordner",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast Check: Wenn Größe und mtime identisch sind, überspringen (ohne --checksum)
|
// Fast Check: Wenn Größe und mtime identisch sind, überspringen (ohne --checksum)
|
||||||
@@ -278,7 +291,8 @@ pub fn sync_single_file_to_vault(
|
|||||||
let node_id = match existing_node {
|
let node_id = match existing_node {
|
||||||
Some(n) => n.id,
|
Some(n) => n.id,
|
||||||
None => {
|
None => {
|
||||||
let new_node = db.create_node_in_vault(vault_id, parent_node_id, file_name, false, dek)?;
|
let new_node =
|
||||||
|
db.create_node_in_vault(vault_id, parent_node_id, file_name, false, dek)?;
|
||||||
new_node.id
|
new_node.id
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -370,8 +384,12 @@ pub fn sync_single_file_to_host(
|
|||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out_file = File::create(local_path)
|
let mut out_file = File::create(local_path).with_context(|| {
|
||||||
.with_context(|| format!("Konnte Zieldatei '{}' nicht erstellen", local_path.display()))?;
|
format!(
|
||||||
|
"Konnte Zieldatei '{}' nicht erstellen",
|
||||||
|
local_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let total_chunks = if node.size == 0 {
|
let total_chunks = if node.size == 0 {
|
||||||
0
|
0
|
||||||
@@ -381,10 +399,22 @@ pub fn sync_single_file_to_host(
|
|||||||
|
|
||||||
for idx in 0..total_chunks {
|
for idx in 0..total_chunks {
|
||||||
if let Some(record) = db.read_chunk(node.id, idx)? {
|
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)?;
|
let plaintext = decrypt_chunk(
|
||||||
|
dek,
|
||||||
|
node.id,
|
||||||
|
idx,
|
||||||
|
&record.ciphertext,
|
||||||
|
&record.nonce,
|
||||||
|
&record.tag,
|
||||||
|
version,
|
||||||
|
)?;
|
||||||
out_file.write_all(&plaintext)?;
|
out_file.write_all(&plaintext)?;
|
||||||
} else {
|
} else {
|
||||||
bail!("Beschädigte Datei im Tresor: Chunk #{} für Knoten '{}' fehlt", idx, node.name);
|
bail!(
|
||||||
|
"Beschädigte Datei im Tresor: Chunk #{} für Knoten '{}' fehlt",
|
||||||
|
idx,
|
||||||
|
node.name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,10 +439,14 @@ pub fn run_sync(
|
|||||||
|
|
||||||
match options.direction {
|
match options.direction {
|
||||||
SyncDirection::Push => {
|
SyncDirection::Push => {
|
||||||
sync_push(db, vault_id, dek, version, source_arg, target_arg, options, &mut stats)?;
|
sync_push(
|
||||||
|
db, vault_id, dek, version, source_arg, target_arg, options, &mut stats,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
SyncDirection::Pull => {
|
SyncDirection::Pull => {
|
||||||
sync_pull(db, vault_id, dek, version, source_arg, target_arg, options, &mut stats)?;
|
sync_pull(
|
||||||
|
db, vault_id, dek, version, source_arg, target_arg, options, &mut stats,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +470,11 @@ fn sync_push(
|
|||||||
bail!("Lokale Quelle '{}' existiert nicht.", source_str);
|
bail!("Lokale Quelle '{}' existiert nicht.", source_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_vault_dir = if target_str.is_empty() { "/" } else { target_str };
|
let target_vault_dir = if target_str.is_empty() {
|
||||||
|
"/"
|
||||||
|
} else {
|
||||||
|
target_str
|
||||||
|
};
|
||||||
|
|
||||||
if local_source.is_file() {
|
if local_source.is_file() {
|
||||||
let file_name = local_source
|
let file_name = local_source
|
||||||
@@ -469,14 +507,24 @@ fn sync_push(
|
|||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} Übertragen: {} ({})", ui::green("[+]"), file_name, ui::format_bytes(size));
|
println!(
|
||||||
|
" {} Übertragen: {} ({})",
|
||||||
|
ui::green("[+]"),
|
||||||
|
file_name,
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::DryRunTransferred { size } => {
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} [DRY-RUN] Würde übertragen: {} ({})", ui::yellow("[~]"), file_name, ui::format_bytes(size));
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde übertragen: {} ({})",
|
||||||
|
ui::yellow("[~]"),
|
||||||
|
file_name,
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::Skipped { .. } => {
|
FileTransferResult::Skipped { .. } => {
|
||||||
@@ -556,11 +604,20 @@ fn collect_and_push_dir(
|
|||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
// Ordner im Tresor anlegen falls nötig
|
// Ordner im Tresor anlegen falls nötig
|
||||||
let children = db.list_children_in_vault(current_vault_parent_id, vault_id, dek)?;
|
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) {
|
let sub_dir_node = match children
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.name == file_name && c.is_dir)
|
||||||
|
{
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => {
|
None => {
|
||||||
if !options.dry_run {
|
if !options.dry_run {
|
||||||
db.create_node_in_vault(vault_id, current_vault_parent_id, &file_name, true, dek)?
|
db.create_node_in_vault(
|
||||||
|
vault_id,
|
||||||
|
current_vault_parent_id,
|
||||||
|
&file_name,
|
||||||
|
true,
|
||||||
|
dek,
|
||||||
|
)?
|
||||||
} else {
|
} else {
|
||||||
// Dummy für dry-run
|
// Dummy für dry-run
|
||||||
NodeRecord {
|
NodeRecord {
|
||||||
@@ -605,14 +662,24 @@ fn collect_and_push_dir(
|
|||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} Übertragen: {} ({})", ui::green("[+]"), rel_path, ui::format_bytes(size));
|
println!(
|
||||||
|
" {} Übertragen: {} ({})",
|
||||||
|
ui::green("[+]"),
|
||||||
|
rel_path,
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::DryRunTransferred { size } => {
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} [DRY-RUN] Würde übertragen: {} ({})", ui::yellow("[~]"), rel_path, ui::format_bytes(size));
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde übertragen: {} ({})",
|
||||||
|
ui::yellow("[~]"),
|
||||||
|
rel_path,
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::Skipped { .. } => {
|
FileTransferResult::Skipped { .. } => {
|
||||||
@@ -644,7 +711,10 @@ pub fn delete_orphans_in_vault(
|
|||||||
|
|
||||||
for child in children {
|
for child in children {
|
||||||
// S-03 Carrier Guard: Trägerdatei und übergeordnete Verzeichnisse niemals löschen!
|
// S-03 Carrier Guard: Trägerdatei und übergeordnete Verzeichnisse niemals löschen!
|
||||||
if carrier_id > 0 && (child.id == carrier_id || db.is_descendant_of(carrier_id, child.id).unwrap_or(false)) {
|
if carrier_id > 0
|
||||||
|
&& (child.id == carrier_id
|
||||||
|
|| db.is_descendant_of(carrier_id, child.id).unwrap_or(false))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +728,11 @@ pub fn delete_orphans_in_vault(
|
|||||||
stats.files_deleted += 1;
|
stats.files_deleted += 1;
|
||||||
if dry_run {
|
if dry_run {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
println!(" {} [DRY-RUN] Würde aus Tresor löschen: {}", ui::red("[-]"), child_rel);
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde aus Tresor löschen: {}",
|
||||||
|
ui::red("[-]"),
|
||||||
|
child_rel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
db.delete_node(child.id)?;
|
db.delete_node(child.id)?;
|
||||||
@@ -724,20 +798,34 @@ fn sync_pull(
|
|||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} Wiederhergestellt: {} ({})", ui::green("[+]"), local_file_path.display(), ui::format_bytes(size));
|
println!(
|
||||||
|
" {} Wiederhergestellt: {} ({})",
|
||||||
|
ui::green("[+]"),
|
||||||
|
local_file_path.display(),
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::DryRunTransferred { size } => {
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} [DRY-RUN] Würde wiederherstellen: {} ({})", ui::yellow("[~]"), local_file_path.display(), ui::format_bytes(size));
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde wiederherstellen: {} ({})",
|
||||||
|
ui::yellow("[~]"),
|
||||||
|
local_file_path.display(),
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::Skipped { .. } => {
|
FileTransferResult::Skipped { .. } => {
|
||||||
stats.files_skipped += 1;
|
stats.files_skipped += 1;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} Aktuell (übersprungen): {}", ui::dim("[=]"), local_file_path.display());
|
println!(
|
||||||
|
" {} Aktuell (übersprungen): {}",
|
||||||
|
ui::dim("[=]"),
|
||||||
|
local_file_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,7 +883,10 @@ fn collect_and_pull_dir(
|
|||||||
|
|
||||||
for child in children {
|
for child in children {
|
||||||
// R-02 Carrier Guard: Trägerdatei niemals auf den Host spiegeln / herausziehen
|
// R-02 Carrier Guard: Trägerdatei niemals auf den Host spiegeln / herausziehen
|
||||||
if carrier_id > 0 && (child.id == carrier_id || db.is_descendant_of(carrier_id, child.id).unwrap_or(false)) {
|
if carrier_id > 0
|
||||||
|
&& (child.id == carrier_id
|
||||||
|
|| db.is_descendant_of(carrier_id, child.id).unwrap_or(false))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +907,10 @@ fn collect_and_pull_dir(
|
|||||||
for comp in Path::new(&child_rel).components() {
|
for comp in Path::new(&child_rel).components() {
|
||||||
match comp {
|
match comp {
|
||||||
std::path::Component::Normal(_) => {}
|
std::path::Component::Normal(_) => {}
|
||||||
_ => bail!("Path traversal Versuch erkannt in relativem Pfad: '{}'", child_rel),
|
_ => bail!(
|
||||||
|
"Path traversal Versuch erkannt in relativem Pfad: '{}'",
|
||||||
|
child_rel
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +918,10 @@ fn collect_and_pull_dir(
|
|||||||
let local_child_path = local_target_base.join(&child_rel.replace('/', "\\"));
|
let local_child_path = local_target_base.join(&child_rel.replace('/', "\\"));
|
||||||
|
|
||||||
if !local_child_path.starts_with(local_target_base) {
|
if !local_child_path.starts_with(local_target_base) {
|
||||||
bail!("Path traversal Versuch erkannt: '{}' bricht aus Zielverzeichnis aus", child_rel);
|
bail!(
|
||||||
|
"Path traversal Versuch erkannt: '{}' bricht aus Zielverzeichnis aus",
|
||||||
|
child_rel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if child.is_dir {
|
if child.is_dir {
|
||||||
@@ -859,14 +956,24 @@ fn collect_and_pull_dir(
|
|||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} Wiederhergestellt: {} ({})", ui::green("[+]"), child_rel, ui::format_bytes(size));
|
println!(
|
||||||
|
" {} Wiederhergestellt: {} ({})",
|
||||||
|
ui::green("[+]"),
|
||||||
|
child_rel,
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::DryRunTransferred { size } => {
|
FileTransferResult::DryRunTransferred { size } => {
|
||||||
stats.files_transferred += 1;
|
stats.files_transferred += 1;
|
||||||
stats.bytes_transferred += size;
|
stats.bytes_transferred += size;
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
println!(" {} [DRY-RUN] Würde wiederherstellen: {} ({})", ui::yellow("[~]"), child_rel, ui::format_bytes(size));
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde wiederherstellen: {} ({})",
|
||||||
|
ui::yellow("[~]"),
|
||||||
|
child_rel,
|
||||||
|
ui::format_bytes(size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileTransferResult::Skipped { .. } => {
|
FileTransferResult::Skipped { .. } => {
|
||||||
@@ -908,7 +1015,11 @@ fn delete_orphans_on_host(
|
|||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
if dry_run {
|
if dry_run {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
println!(" {} [DRY-RUN] Würde lokalen Ordner löschen: {}", ui::red("[-]"), rel_path);
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde lokalen Ordner löschen: {}",
|
||||||
|
ui::red("[-]"),
|
||||||
|
rel_path
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs::remove_dir_all(&path)?;
|
fs::remove_dir_all(&path)?;
|
||||||
@@ -919,7 +1030,11 @@ fn delete_orphans_on_host(
|
|||||||
} else {
|
} else {
|
||||||
if dry_run {
|
if dry_run {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
println!(" {} [DRY-RUN] Würde lokale Datei löschen: {}", ui::red("[-]"), rel_path);
|
println!(
|
||||||
|
" {} [DRY-RUN] Würde lokale Datei löschen: {}",
|
||||||
|
ui::red("[-]"),
|
||||||
|
rel_path
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs::remove_file(&path)?;
|
fs::remove_file(&path)?;
|
||||||
@@ -951,10 +1066,18 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
assert!(is_excluded("file.tmp", "sub/file.tmp", &patterns));
|
assert!(is_excluded("file.tmp", "sub/file.tmp", &patterns));
|
||||||
assert!(is_excluded("download.crdownload", "download.crdownload", &patterns));
|
assert!(is_excluded(
|
||||||
|
"download.crdownload",
|
||||||
|
"download.crdownload",
|
||||||
|
&patterns
|
||||||
|
));
|
||||||
assert!(is_excluded("Thumbs.db", "Thumbs.db", &patterns));
|
assert!(is_excluded("Thumbs.db", "Thumbs.db", &patterns));
|
||||||
assert!(is_excluded("backup_2026.tar", "backup_2026.tar", &patterns));
|
assert!(is_excluded("backup_2026.tar", "backup_2026.tar", &patterns));
|
||||||
assert!(!is_excluded("important.doc", "sub/important.doc", &patterns));
|
assert!(!is_excluded(
|
||||||
|
"important.doc",
|
||||||
|
"sub/important.doc",
|
||||||
|
&patterns
|
||||||
|
));
|
||||||
assert!(!is_excluded("video.mp4", "video.mp4", &patterns));
|
assert!(!is_excluded("video.mp4", "video.mp4", &patterns));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,13 +131,25 @@ pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
|||||||
pub fn print_recovery_phrase_card(phrase: &str) {
|
pub fn print_recovery_phrase_card(phrase: &str) {
|
||||||
let words: Vec<&str> = phrase.split_whitespace().collect();
|
let words: Vec<&str> = phrase.split_whitespace().collect();
|
||||||
println!();
|
println!();
|
||||||
println!("{}", yellow("┌─────────────────────────────────────────────────────────────┐"));
|
println!(
|
||||||
println!("{}", yellow("│ ⚠️ 24-WORT NOTFALL-WIEDERHERSTELLUNGSSCHLÜSSEL │"));
|
"{}",
|
||||||
println!("{}", yellow("├─────────────────────────────────────────────────────────────┤"));
|
yellow("┌─────────────────────────────────────────────────────────────┐")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
yellow("│ ⚠️ 24-WORT NOTFALL-WIEDERHERSTELLUNGSSCHLÜSSEL │")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
yellow("├─────────────────────────────────────────────────────────────┤")
|
||||||
|
);
|
||||||
println!("│ Falls Sie Ihr Master-Passwort vergessen oder der Header │");
|
println!("│ Falls Sie Ihr Master-Passwort vergessen oder der Header │");
|
||||||
println!("│ beschädigt wird, ist dies Ihre EINZIGE Rettung! │");
|
println!("│ beschädigt wird, ist dies Ihre EINZIGE Rettung! │");
|
||||||
println!("│ Notieren Sie die Wörter in EXAKTER Reihenfolge auf Papier! │");
|
println!("│ Notieren Sie die Wörter in EXAKTER Reihenfolge auf Papier! │");
|
||||||
println!("{}", yellow("├─────────────────────────────────────────────────────────────┤"));
|
println!(
|
||||||
|
"{}",
|
||||||
|
yellow("├─────────────────────────────────────────────────────────────┤")
|
||||||
|
);
|
||||||
|
|
||||||
for row in 0..8 {
|
for row in 0..8 {
|
||||||
let w1 = if row < words.len() {
|
let w1 = if row < words.len() {
|
||||||
@@ -158,7 +170,10 @@ pub fn print_recovery_phrase_card(phrase: &str) {
|
|||||||
println!("│ {:<18} {:<18} {:<18} │", cyan(&w1), cyan(&w2), cyan(&w3));
|
println!("│ {:<18} {:<18} {:<18} │", cyan(&w1), cyan(&w2), cyan(&w3));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{}", yellow("└─────────────────────────────────────────────────────────────┘"));
|
println!(
|
||||||
|
"{}",
|
||||||
|
yellow("└─────────────────────────────────────────────────────────────┘")
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +219,10 @@ pub fn print_verification_report(report: &crate::verify::VerificationReport) {
|
|||||||
println!(" - Daten-Chunks: {}", report.total_chunks);
|
println!(" - Daten-Chunks: {}", report.total_chunks);
|
||||||
if report.total_bytes_decrypted > 0 {
|
if report.total_bytes_decrypted > 0 {
|
||||||
let mb = report.total_bytes_decrypted as f64 / (1024.0 * 1024.0);
|
let mb = report.total_bytes_decrypted as f64 / (1024.0 * 1024.0);
|
||||||
println!(" - Verifiziert: {:.2} MB (vollständig entschlüsselt)", mb);
|
println!(
|
||||||
|
" - Verifiziert: {:.2} MB (vollständig entschlüsselt)",
|
||||||
|
mb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !report.errors.is_empty() {
|
if !report.errors.is_empty() {
|
||||||
@@ -217,10 +235,15 @@ pub fn print_verification_report(report: &crate::verify::VerificationReport) {
|
|||||||
|
|
||||||
println!();
|
println!();
|
||||||
if report.is_healthy() {
|
if report.is_healthy() {
|
||||||
println!(" {}", green("✔ Keine Beschädigungen oder Bitrot festgestellt. Der Container ist integer."));
|
println!(
|
||||||
|
" {}",
|
||||||
|
green("✔ Keine Beschädigungen oder Bitrot festgestellt. Der Container ist integer.")
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(" {}", red("✖ ACHTUNG: Der Container weist Beschädigungen auf! Bitte Backup prüfen."));
|
println!(
|
||||||
|
" {}",
|
||||||
|
red("✖ ACHTUNG: Der Container weist Beschädigungen auf! Bitte Backup prüfen.")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+69
-25
@@ -65,11 +65,7 @@ impl Default for UpgradeOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Überprüft eine Minisign-Signatur für gegebene Binärdaten mit dem angegebenen Minisign Public Key (S-02).
|
/// Überprüft eine Minisign-Signatur für gegebene Binärdaten mit dem angegebenen Minisign Public Key (S-02).
|
||||||
pub fn verify_minisign_signature(
|
pub fn verify_minisign_signature(data: &[u8], sig_str: &str, pubkey_str: &str) -> Result<()> {
|
||||||
data: &[u8],
|
|
||||||
sig_str: &str,
|
|
||||||
pubkey_str: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let pubkey = minisign_verify::PublicKey::from_base64(pubkey_str)
|
let pubkey = minisign_verify::PublicKey::from_base64(pubkey_str)
|
||||||
.map_err(|e| anyhow::anyhow!("Ungültiger öffentlicher Minisign-Schlüssel: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Ungültiger öffentlicher Minisign-Schlüssel: {e}"))?;
|
||||||
let signature = minisign_verify::Signature::decode(sig_str)
|
let signature = minisign_verify::Signature::decode(sig_str)
|
||||||
@@ -138,7 +134,10 @@ pub fn fetch_latest_release(base_url: &str) -> Result<GiteaRelease> {
|
|||||||
let api_url = format!("{}/api/v1/repos/harald/sanctum/releases/latest", base);
|
let api_url = format!("{}/api/v1/repos/harald/sanctum/releases/latest", base);
|
||||||
|
|
||||||
let resp = ureq::get(&api_url)
|
let resp = ureq::get(&api_url)
|
||||||
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
.set(
|
||||||
|
"User-Agent",
|
||||||
|
&format!("sanctum/{}", env!("CARGO_PKG_VERSION")),
|
||||||
|
)
|
||||||
.set("Accept", "application/json")
|
.set("Accept", "application/json")
|
||||||
.timeout(Duration::from_secs(15))
|
.timeout(Duration::from_secs(15))
|
||||||
.call()
|
.call()
|
||||||
@@ -154,7 +153,10 @@ pub fn fetch_latest_release(base_url: &str) -> Result<GiteaRelease> {
|
|||||||
/// Lädt eine Datei von einer URL als Text herunter.
|
/// Lädt eine Datei von einer URL als Text herunter.
|
||||||
pub fn fetch_text(url: &str) -> Result<String> {
|
pub fn fetch_text(url: &str) -> Result<String> {
|
||||||
let resp = ureq::get(url)
|
let resp = ureq::get(url)
|
||||||
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
.set(
|
||||||
|
"User-Agent",
|
||||||
|
&format!("sanctum/{}", env!("CARGO_PKG_VERSION")),
|
||||||
|
)
|
||||||
.timeout(Duration::from_secs(15))
|
.timeout(Duration::from_secs(15))
|
||||||
.call()
|
.call()
|
||||||
.with_context(|| format!("Fehler beim Herunterladen von '{}'", url))?;
|
.with_context(|| format!("Fehler beim Herunterladen von '{}'", url))?;
|
||||||
@@ -212,7 +214,10 @@ pub fn download_and_verify_binary(
|
|||||||
expected_size: u64,
|
expected_size: u64,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<PathBuf> {
|
||||||
let resp = ureq::get(download_url)
|
let resp = ureq::get(download_url)
|
||||||
.set("User-Agent", &format!("sanctum/{}", env!("CARGO_PKG_VERSION")))
|
.set(
|
||||||
|
"User-Agent",
|
||||||
|
&format!("sanctum/{}", env!("CARGO_PKG_VERSION")),
|
||||||
|
)
|
||||||
.timeout(Duration::from_secs(120))
|
.timeout(Duration::from_secs(120))
|
||||||
.call()
|
.call()
|
||||||
.with_context(|| format!("Fehler beim Starten des Downloads von '{}'", download_url))?;
|
.with_context(|| format!("Fehler beim Starten des Downloads von '{}'", download_url))?;
|
||||||
@@ -285,7 +290,10 @@ pub fn download_and_verify_binary(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" • SHA-256: {} (verifiziert)", ui::green(&calculated_hash[..16]));
|
println!(
|
||||||
|
" • SHA-256: {} (verifiziert)",
|
||||||
|
ui::green(&calculated_hash[..16])
|
||||||
|
);
|
||||||
|
|
||||||
let (_, path) = temp_file
|
let (_, path) = temp_file
|
||||||
.keep()
|
.keep()
|
||||||
@@ -324,12 +332,16 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
|||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ Sanctum — In-Place Upgrade & Aktualisierungsdienst │");
|
println!("│ Sanctum — In-Place Upgrade & Aktualisierungsdienst │");
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" • Aktuelle Version: {}", ui::cyan(&format!("v{}", current_version)));
|
println!(
|
||||||
|
" • Aktuelle Version: {}",
|
||||||
|
ui::cyan(&format!("v{}", current_version))
|
||||||
|
);
|
||||||
println!(" • Server: {}", options.base_url);
|
println!(" • Server: {}", options.base_url);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// S-02: Prüfung auf abweichende Server-URL
|
// S-02: Prüfung auf abweichende Server-URL
|
||||||
let is_official_server = options.base_url.trim_end_matches('/') == OFFICIAL_UPGRADE_URL.trim_end_matches('/');
|
let is_official_server =
|
||||||
|
options.base_url.trim_end_matches('/') == OFFICIAL_UPGRADE_URL.trim_end_matches('/');
|
||||||
if !is_official_server {
|
if !is_official_server {
|
||||||
if !options.insecure_url {
|
if !options.insecure_url {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -389,12 +401,22 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Neuestes Release abfragen
|
// 2. Neuestes Release abfragen
|
||||||
println!(" {} Suche nach neuesten Releases auf Gitea ...", ui::dim("[-]"));
|
println!(
|
||||||
|
" {} Suche nach neuesten Releases auf Gitea ...",
|
||||||
|
ui::dim("[-]")
|
||||||
|
);
|
||||||
let release = fetch_latest_release(&options.base_url)?;
|
let release = fetch_latest_release(&options.base_url)?;
|
||||||
let remote_tag = &release.tag_name;
|
let remote_tag = &release.tag_name;
|
||||||
let is_newer = is_newer_version(current_version, remote_tag);
|
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!(
|
||||||
|
" • Neueste Version: {}",
|
||||||
|
if is_newer {
|
||||||
|
ui::green(remote_tag)
|
||||||
|
} else {
|
||||||
|
ui::cyan(remote_tag)
|
||||||
|
}
|
||||||
|
);
|
||||||
println!(" • Release-Name: {}", release.name);
|
println!(" • Release-Name: {}", release.name);
|
||||||
if let Some(ref pub_at) = release.published_at {
|
if let Some(ref pub_at) = release.published_at {
|
||||||
println!(" • Veröffentlicht: {}", pub_at);
|
println!(" • Veröffentlicht: {}", pub_at);
|
||||||
@@ -459,27 +481,46 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// 4. Prüfsummen und Signatur herunterladen und kryptografisch prüfen (S-02)
|
// 4. Prüfsummen und Signatur herunterladen und kryptografisch prüfen (S-02)
|
||||||
println!(" {} Lade Prüfsummen ({}) herunter ...", ui::dim("[-]"), CHECKSUM_ASSET_NAME);
|
println!(
|
||||||
|
" {} Lade Prüfsummen ({}) herunter ...",
|
||||||
|
ui::dim("[-]"),
|
||||||
|
CHECKSUM_ASSET_NAME
|
||||||
|
);
|
||||||
let sums_content = fetch_text(&checksum_asset.browser_download_url)?;
|
let sums_content = fetch_text(&checksum_asset.browser_download_url)?;
|
||||||
|
|
||||||
println!(" {} Lade Minisign-Signatur ({}) herunter ...", ui::dim("[-]"), CHECKSUM_SIG_ASSET_NAME);
|
println!(
|
||||||
|
" {} Lade Minisign-Signatur ({}) herunter ...",
|
||||||
|
ui::dim("[-]"),
|
||||||
|
CHECKSUM_SIG_ASSET_NAME
|
||||||
|
);
|
||||||
let sig_content = fetch_text(&sig_asset.browser_download_url)?;
|
let sig_content = fetch_text(&sig_asset.browser_download_url)?;
|
||||||
|
|
||||||
println!(" {} Verifiziere Minisign-Signatur gegen eingebetteten Herstellerschlüssel ...", ui::dim("[-]"));
|
println!(
|
||||||
|
" {} Verifiziere Minisign-Signatur gegen eingebetteten Herstellerschlüssel ...",
|
||||||
|
ui::dim("[-]")
|
||||||
|
);
|
||||||
verify_minisign_signature(sums_content.as_bytes(), &sig_content, SANCTUM_RELEASE_PUBKEY)
|
verify_minisign_signature(sums_content.as_bytes(), &sig_content, SANCTUM_RELEASE_PUBKEY)
|
||||||
.context("Minisign-Signaturprüfung für Prüfsummendatei FEHLGESCHLAGEN! Release ist nicht vertrauenswürdig.")?;
|
.context("Minisign-Signaturprüfung für Prüfsummendatei FEHLGESCHLAGEN! Release ist nicht vertrauenswürdig.")?;
|
||||||
println!(" • Signatur: {} (Minisign Ed25519)", ui::green("Gültig"));
|
println!(
|
||||||
|
" • Signatur: {} (Minisign Ed25519)",
|
||||||
|
ui::green("Gültig")
|
||||||
|
);
|
||||||
|
|
||||||
let expected_hash = parse_checksum_for_asset(&sums_content, TARGET_ASSET_NAME).ok_or_else(|| {
|
let expected_hash =
|
||||||
anyhow::anyhow!(
|
parse_checksum_for_asset(&sums_content, TARGET_ASSET_NAME).ok_or_else(|| {
|
||||||
"Prüfsummendatei enthält keinen SHA-256 Eintrag für '{}'!",
|
anyhow::anyhow!(
|
||||||
TARGET_ASSET_NAME
|
"Prüfsummendatei enthält keinen SHA-256 Eintrag für '{}'!",
|
||||||
)
|
TARGET_ASSET_NAME
|
||||||
})?;
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
// 5. Interaktive Bestätigung (sofern nicht -y / --yes)
|
// 5. Interaktive Bestätigung (sofern nicht -y / --yes)
|
||||||
if !options.yes {
|
if !options.yes {
|
||||||
println!(" • Ziel-Binary: {} ({})", target_asset.name, ui::format_bytes(target_asset.size));
|
println!(
|
||||||
|
" • Ziel-Binary: {} ({})",
|
||||||
|
target_asset.name,
|
||||||
|
ui::format_bytes(target_asset.size)
|
||||||
|
);
|
||||||
println!(" • Erwarteter Hash: {}...", &expected_hash[..16]);
|
println!(" • Erwarteter Hash: {}...", &expected_hash[..16]);
|
||||||
println!();
|
println!();
|
||||||
print!(
|
print!(
|
||||||
@@ -513,7 +554,10 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
|||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ ✔ Sanctum wurde erfolgreich auf {} aktualisiert! │", remote_tag);
|
println!(
|
||||||
|
"│ ✔ Sanctum wurde erfolgreich auf {} aktualisiert! │",
|
||||||
|
remote_tag
|
||||||
|
);
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" • Pfad: {}", updated_exe.display());
|
println!(" • Pfad: {}", updated_exe.display());
|
||||||
println!(" • Neue Version: {}", ui::green(remote_tag));
|
println!(" • Neue Version: {}", ui::green(remote_tag));
|
||||||
|
|||||||
+62
-21
@@ -43,11 +43,13 @@ pub fn verify_container(
|
|||||||
full_chunks: bool,
|
full_chunks: bool,
|
||||||
) -> Result<VerificationReport> {
|
) -> Result<VerificationReport> {
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
bail!(
|
||||||
|
"Containerdatei '{}' existiert nicht.",
|
||||||
|
container_path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
|
||||||
|
|
||||||
let mut report = VerificationReport {
|
let mut report = VerificationReport {
|
||||||
container_path: container_path.display().to_string(),
|
container_path: container_path.display().to_string(),
|
||||||
@@ -67,7 +69,8 @@ pub fn verify_container(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 1. SQLite B-Tree & Foreign Key Prüfung
|
// 1. SQLite B-Tree & Foreign Key Prüfung
|
||||||
let sqlite_issues = db.run_sqlite_integrity_check()
|
let sqlite_issues = db
|
||||||
|
.run_sqlite_integrity_check()
|
||||||
.context("Fehler bei der Ausführung des SQLite integrity_check")?;
|
.context("Fehler bei der Ausführung des SQLite integrity_check")?;
|
||||||
if !sqlite_issues.is_empty() {
|
if !sqlite_issues.is_empty() {
|
||||||
report.sqlite_ok = false;
|
report.sqlite_ok = false;
|
||||||
@@ -96,13 +99,16 @@ pub fn verify_container(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 3. Node-Hierarchie & Strukturprüfung
|
// 3. Node-Hierarchie & Strukturprüfung
|
||||||
let (dirs, files, chunks_count) = db.count_nodes_and_chunks()
|
let (dirs, files, chunks_count) = db
|
||||||
|
.count_nodes_and_chunks()
|
||||||
.context("Fehler beim Zählen der Knoten und Chunks")?;
|
.context("Fehler beim Zählen der Knoten und Chunks")?;
|
||||||
report.total_dirs = dirs;
|
report.total_dirs = dirs;
|
||||||
report.total_files = files;
|
report.total_files = files;
|
||||||
report.total_chunks = chunks_count;
|
report.total_chunks = chunks_count;
|
||||||
|
|
||||||
let all_nodes = db.list_all_nodes().context("Fehler beim Laden der Knotenliste")?;
|
let all_nodes = db
|
||||||
|
.list_all_nodes()
|
||||||
|
.context("Fehler beim Laden der Knotenliste")?;
|
||||||
report.total_nodes = all_nodes.len();
|
report.total_nodes = all_nodes.len();
|
||||||
|
|
||||||
let mut node_map = HashMap::new();
|
let mut node_map = HashMap::new();
|
||||||
@@ -114,23 +120,33 @@ pub fn verify_container(
|
|||||||
match node_map.get(&1) {
|
match node_map.get(&1) {
|
||||||
Some(root) => {
|
Some(root) => {
|
||||||
if !root.is_dir {
|
if !root.is_dir {
|
||||||
report.errors.push("Root-Knoten (id=1) ist nicht als Verzeichnis markiert!".to_string());
|
report
|
||||||
|
.errors
|
||||||
|
.push("Root-Knoten (id=1) ist nicht als Verzeichnis markiert!".to_string());
|
||||||
}
|
}
|
||||||
if root.parent_id.is_some() {
|
if root.parent_id.is_some() {
|
||||||
report.errors.push("Root-Knoten (id=1) darf keinen Parent haben!".to_string());
|
report
|
||||||
|
.errors
|
||||||
|
.push("Root-Knoten (id=1) darf keinen Parent haben!".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
report.errors.push("Root-Knoten (id=1) fehlt in der nodes-Tabelle!".to_string());
|
report
|
||||||
|
.errors
|
||||||
|
.push("Root-Knoten (id=1) fehlt in der nodes-Tabelle!".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(root2) = node_map.get(&2) {
|
if let Some(root2) = node_map.get(&2) {
|
||||||
if !root2.is_dir {
|
if !root2.is_dir {
|
||||||
report.errors.push("Root-Knoten (id=2) ist nicht als Verzeichnis markiert!".to_string());
|
report
|
||||||
|
.errors
|
||||||
|
.push("Root-Knoten (id=2) ist nicht als Verzeichnis markiert!".to_string());
|
||||||
}
|
}
|
||||||
if root2.parent_id.is_some() {
|
if root2.parent_id.is_some() {
|
||||||
report.errors.push("Root-Knoten (id=2) darf keinen Parent haben!".to_string());
|
report
|
||||||
|
.errors
|
||||||
|
.push("Root-Knoten (id=2) darf keinen Parent haben!".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,10 +248,14 @@ pub fn verify_container(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 4. Kryptografische Chunk- & AEAD-Authentifizierungsprüfung
|
// 4. Kryptografische Chunk- & AEAD-Authentifizierungsprüfung
|
||||||
let chunk_headers = db.list_all_chunk_headers()
|
let chunk_headers = db
|
||||||
|
.list_all_chunk_headers()
|
||||||
.context("Fehler beim Abrufen der Chunk-Liste")?;
|
.context("Fehler beim Abrufen der Chunk-Liste")?;
|
||||||
|
|
||||||
let format_version = meta.as_ref().map(|m| m.version).unwrap_or(FORMAT_VERSION_V2);
|
let format_version = meta
|
||||||
|
.as_ref()
|
||||||
|
.map(|m| m.version)
|
||||||
|
.unwrap_or(FORMAT_VERSION_V2);
|
||||||
|
|
||||||
for (node_id, chunk_index) in chunk_headers {
|
for (node_id, chunk_index) in chunk_headers {
|
||||||
if !node_map.contains_key(&node_id) {
|
if !node_map.contains_key(&node_id) {
|
||||||
@@ -319,7 +339,8 @@ mod tests {
|
|||||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&container_path).unwrap();
|
let db = Database::open(&container_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Verzeichnis & Datei anlegen
|
// Verzeichnis & Datei anlegen
|
||||||
let folder = db.create_node(1, "photos", true).unwrap();
|
let folder = db.create_node(1, "photos", true).unwrap();
|
||||||
@@ -334,12 +355,21 @@ mod tests {
|
|||||||
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION).unwrap();
|
let (ct1, n1, t1) = encrypt_chunk(&dek, file.id, 1, chunk1_data, FORMAT_VERSION).unwrap();
|
||||||
db.write_chunk(file.id, 1, &n1, &t1, &ct1).unwrap();
|
db.write_chunk(file.id, 1, &n1, &t1, &ct1).unwrap();
|
||||||
|
|
||||||
db.update_node_size_and_time(file.id, (chunk0_data.len() + chunk1_data.len()) as u64, 1000).unwrap();
|
db.update_node_size_and_time(
|
||||||
|
file.id,
|
||||||
|
(chunk0_data.len() + chunk1_data.len()) as u64,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
// Verifizieren
|
// Verifizieren
|
||||||
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
|
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
|
||||||
assert!(report.is_healthy(), "Container must be healthy, report: {:?}", report);
|
assert!(
|
||||||
|
report.is_healthy(),
|
||||||
|
"Container must be healthy, report: {:?}",
|
||||||
|
report
|
||||||
|
);
|
||||||
assert_eq!(report.total_files, 1);
|
assert_eq!(report.total_files, 1);
|
||||||
assert_eq!(report.total_dirs, 3); // Root 1 + Root 2 (Dual-Vault) + photos
|
assert_eq!(report.total_dirs, 3); // Root 1 + Root 2 (Dual-Vault) + photos
|
||||||
assert_eq!(report.total_chunks, 2);
|
assert_eq!(report.total_chunks, 2);
|
||||||
@@ -371,7 +401,8 @@ mod tests {
|
|||||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&container_path).unwrap();
|
let db = Database::open(&container_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let file = db.create_node(1, "document.pdf", false).unwrap();
|
let file = db.create_node(1, "document.pdf", false).unwrap();
|
||||||
let chunk_data = b"Vital documents that must not be corrupted";
|
let chunk_data = b"Vital documents that must not be corrupted";
|
||||||
@@ -387,14 +418,24 @@ mod tests {
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = 0",
|
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = 0",
|
||||||
rusqlite::params![corrupted_ct, file.id],
|
rusqlite::params![corrupted_ct, file.id],
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
// Verifizieren: Muss Bitrot via AEAD Tag-Fehler sofort entlarven!
|
// Verifizieren: Muss Bitrot via AEAD Tag-Fehler sofort entlarven!
|
||||||
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
|
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
|
||||||
assert!(!report.is_healthy(), "Container must report unhealthy due to bitrot");
|
assert!(
|
||||||
assert_eq!(report.corrupted_chunks, 1, "Must detect exactly 1 corrupted chunk");
|
!report.is_healthy(),
|
||||||
assert!(report.errors.iter().any(|e| e.contains("AEAD/Integritätsfehler")));
|
"Container must report unhealthy due to bitrot"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.corrupted_chunks, 1,
|
||||||
|
"Must detect exactly 1 corrupted chunk"
|
||||||
|
);
|
||||||
|
assert!(report
|
||||||
|
.errors
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.contains("AEAD/Integritätsfehler")));
|
||||||
|
|
||||||
let _ = fs::remove_file(&container_path);
|
let _ = fs::remove_file(&container_path);
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-62
@@ -26,14 +26,8 @@ use crate::storage::{Database, NodeRecord};
|
|||||||
pub fn is_leak_file(filename: &str) -> bool {
|
pub fn is_leak_file(filename: &str) -> bool {
|
||||||
let lower = filename.trim().to_ascii_lowercase();
|
let lower = filename.trim().to_ascii_lowercase();
|
||||||
match lower.as_str() {
|
match lower.as_str() {
|
||||||
"thumbs.db"
|
"thumbs.db" | "ehthumbs.db" | "ehthumbs_vista.db" | "desktop.ini" | "folder.jpg"
|
||||||
| "ehthumbs.db"
|
| "albumartsmall.jpg" | "autorun.inf" | ".ds_store" => true,
|
||||||
| "ehthumbs_vista.db"
|
|
||||||
| "desktop.ini"
|
|
||||||
| "folder.jpg"
|
|
||||||
| "albumartsmall.jpg"
|
|
||||||
| "autorun.inf"
|
|
||||||
| ".ds_store" => true,
|
|
||||||
_ => {
|
_ => {
|
||||||
if lower.starts_with("albumart") && (lower.ends_with(".jpg") || lower.ends_with(".ini"))
|
if lower.starts_with("albumart") && (lower.ends_with(".jpg") || lower.ends_with(".ini"))
|
||||||
{
|
{
|
||||||
@@ -167,7 +161,6 @@ impl SanctumFile {
|
|||||||
self.last_activity.store(now, Ordering::Relaxed);
|
self.last_activity.store(now, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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
|
||||||
/// und aktualisiert Dateigröße und Modifikationszeitstempel atomar in einer Transaktion (CHAOS-01).
|
/// 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).
|
/// Bei Fehlern (z. B. Disk Full) wird der Cache sauber invalidiert (CHAOS-03).
|
||||||
@@ -179,10 +172,12 @@ impl SanctumFile {
|
|||||||
|
|
||||||
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(
|
||||||
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
|e| {
|
||||||
FsError::GeneralFailure
|
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
||||||
})?;
|
FsError::GeneralFailure
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
if let Err(e) = self.db.write_chunk_and_update_size(
|
if let Err(e) = self.db.write_chunk_and_update_size(
|
||||||
self.node_id,
|
self.node_id,
|
||||||
@@ -207,7 +202,10 @@ impl SanctumFile {
|
|||||||
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||||
} else if self.meta.size != self.file_size {
|
} 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
|
// 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) {
|
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}");
|
error!("Fehler beim Aktualisieren der Knotengröße: {e}");
|
||||||
return Err(FsError::GeneralFailure);
|
return Err(FsError::GeneralFailure);
|
||||||
}
|
}
|
||||||
@@ -263,7 +261,10 @@ 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_and_size() {
|
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();
|
||||||
@@ -354,7 +355,12 @@ 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_and_size()?;
|
self.flush_cached_chunk_and_size()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,14 +639,19 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.touch();
|
self.touch();
|
||||||
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
debug!(
|
||||||
|
"VFS open aufgerufen: path='{}', options={:?}",
|
||||||
|
path_str, options
|
||||||
|
);
|
||||||
|
|
||||||
let existing_node = self.resolve_path(&path_str)?;
|
let existing_node = self.resolve_path(&path_str)?;
|
||||||
|
|
||||||
let node = match existing_node {
|
let node = match existing_node {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
// Schutz der Trägerdatei im Decoy Vault: Keine Schreib- oder Truncate-Operationen erlaubt!
|
// Schutz der Trägerdatei im Decoy Vault: Keine Schreib- oder Truncate-Operationen erlaubt!
|
||||||
if self.carrier_node_id == Some(n.id) && (options.write || options.truncate || options.append) {
|
if self.carrier_node_id == Some(n.id)
|
||||||
|
&& (options.write || options.truncate || options.append)
|
||||||
|
{
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,9 +685,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if options.create || options.create_new {
|
if options.create || options.create_new {
|
||||||
let parent = self
|
let parent = self.resolve_path(parent_path)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(parent_path)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
if !parent.is_dir {
|
if !parent.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
@@ -712,9 +721,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(&path_str)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
if !node.is_dir {
|
if !node.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
@@ -749,9 +756,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(&path_str)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
let meta = SanctumMetaData {
|
let meta = SanctumMetaData {
|
||||||
is_dir: node.is_dir,
|
is_dir: node.is_dir,
|
||||||
@@ -789,9 +794,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
return Err(FsError::Exists);
|
return Err(FsError::Exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parent = self
|
let parent = self.resolve_path(parent_path)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(parent_path)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
if !parent.is_dir {
|
if !parent.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
@@ -811,9 +814,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.touch();
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(&path_str)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
if !node.is_dir {
|
if !node.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
@@ -826,7 +827,11 @@ impl DavFileSystem for SanctumFs {
|
|||||||
|
|
||||||
// Schutz der Trägerdatei im Decoy Vault: Verzeichnis darf nicht gelöscht werden, wenn es den Carrier enthält!
|
// 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 let Some(carrier_id) = self.carrier_node_id {
|
||||||
if self.db.is_descendant_of(carrier_id, node.id).map_err(|_| FsError::GeneralFailure)? {
|
if self
|
||||||
|
.db
|
||||||
|
.is_descendant_of(carrier_id, node.id)
|
||||||
|
.map_err(|_| FsError::GeneralFailure)?
|
||||||
|
{
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,9 +852,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.touch();
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(&path_str)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
if node.is_dir {
|
if node.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
@@ -868,11 +871,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename<'a>(
|
fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
|
||||||
&'a self,
|
|
||||||
from: &'a DavPath,
|
|
||||||
to: &'a DavPath,
|
|
||||||
) -> FsFuture<'a, ()> {
|
|
||||||
if let Some(ref cfs) = self.carrier_fs {
|
if let Some(ref cfs) = self.carrier_fs {
|
||||||
return cfs.rename(from, to);
|
return cfs.rename(from, to);
|
||||||
}
|
}
|
||||||
@@ -882,9 +881,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
let from_str = Self::path_to_str(from);
|
let from_str = Self::path_to_str(from);
|
||||||
let to_str = Self::path_to_str(to);
|
let to_str = Self::path_to_str(to);
|
||||||
|
|
||||||
let node = self
|
let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(&from_str)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
// Schutz der Trägerdatei im Decoy Vault: Umbenennen verboten!
|
// Schutz der Trägerdatei im Decoy Vault: Umbenennen verboten!
|
||||||
if self.carrier_node_id == Some(node.id) {
|
if self.carrier_node_id == Some(node.id) {
|
||||||
@@ -925,19 +922,13 @@ impl DavFileSystem for SanctumFs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy<'a>(
|
fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
|
||||||
&'a self,
|
|
||||||
from: &'a DavPath,
|
|
||||||
to: &'a DavPath,
|
|
||||||
) -> FsFuture<'a, ()> {
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.touch();
|
self.touch();
|
||||||
let from_str = Self::path_to_str(from);
|
let from_str = Self::path_to_str(from);
|
||||||
let to_str = Self::path_to_str(to);
|
let to_str = Self::path_to_str(to);
|
||||||
|
|
||||||
let node = self
|
let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?;
|
||||||
.resolve_path(&from_str)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
|
||||||
|
|
||||||
if node.is_dir {
|
if node.is_dir {
|
||||||
return Err(FsError::NotImplemented);
|
return Err(FsError::NotImplemented);
|
||||||
@@ -991,9 +982,14 @@ impl DavFileSystem for SanctumFs {
|
|||||||
)
|
)
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
|
|
||||||
let (new_ct, new_nonce, new_tag) =
|
let (new_ct, new_nonce, new_tag) = encrypt_chunk(
|
||||||
encrypt_chunk(&self.dek, dest_node.id, idx, &plaintext, self.format_version)
|
&self.dek,
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
dest_node.id,
|
||||||
|
idx,
|
||||||
|
&plaintext,
|
||||||
|
self.format_version,
|
||||||
|
)
|
||||||
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
|
|
||||||
self.db
|
self.db
|
||||||
.write_chunk(dest_node.id, idx, &new_nonce, &new_tag, &new_ct)
|
.write_chunk(dest_node.id, idx, &new_nonce, &new_tag, &new_ct)
|
||||||
@@ -1025,7 +1021,9 @@ impl DavFileSystem for SanctumFs {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION};
|
use crate::crypto::{
|
||||||
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION,
|
||||||
|
};
|
||||||
use dav_server::fs::OpenOptions;
|
use dav_server::fs::OpenOptions;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
@@ -1078,7 +1076,8 @@ mod tests {
|
|||||||
pub struct TempDir(PathBuf);
|
pub struct TempDir(PathBuf);
|
||||||
impl TempDir {
|
impl TempDir {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let p = std::env::temp_dir().join(format!("sanctum_test_{}", rand::random::<u64>()));
|
let p =
|
||||||
|
std::env::temp_dir().join(format!("sanctum_test_{}", rand::random::<u64>()));
|
||||||
std::fs::create_dir_all(&p).unwrap();
|
std::fs::create_dir_all(&p).unwrap();
|
||||||
Self(p)
|
Self(p)
|
||||||
}
|
}
|
||||||
@@ -1137,7 +1136,10 @@ mod tests {
|
|||||||
|
|
||||||
// read_dir mit anti_leak = true darf Thumbs.db NICHT anzeigen
|
// read_dir mit anti_leak = true darf Thumbs.db NICHT anzeigen
|
||||||
let root_path = DavPath::new("/").unwrap();
|
let root_path = DavPath::new("/").unwrap();
|
||||||
let mut stream = fs_shielded.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
let mut stream = fs_shielded
|
||||||
|
.read_dir(&root_path, ReadDirMeta::None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
while let Some(entry) = stream.next().await {
|
while let Some(entry) = stream.next().await {
|
||||||
let entry = entry.unwrap();
|
let entry = entry.unwrap();
|
||||||
@@ -1154,7 +1156,10 @@ mod tests {
|
|||||||
FORMAT_VERSION,
|
FORMAT_VERSION,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
let mut stream_unshielded = fs_unshielded.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
let mut stream_unshielded = fs_unshielded
|
||||||
|
.read_dir(&root_path, ReadDirMeta::None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let mut names_unshielded = Vec::new();
|
let mut names_unshielded = Vec::new();
|
||||||
while let Some(entry) = stream_unshielded.next().await {
|
while let Some(entry) = stream_unshielded.next().await {
|
||||||
let entry = entry.unwrap();
|
let entry = entry.unwrap();
|
||||||
@@ -1263,7 +1268,13 @@ mod tests {
|
|||||||
|
|
||||||
// 1. Null-Byte im Pfad
|
// 1. Null-Byte im Pfad
|
||||||
let null_path = DavPath::new("/bad\0file.txt");
|
let null_path = DavPath::new("/bad\0file.txt");
|
||||||
assert!(null_path.is_err() || fs.open(&null_path.unwrap(), OpenOptions::default()).await.is_err());
|
assert!(
|
||||||
|
null_path.is_err()
|
||||||
|
|| fs
|
||||||
|
.open(&null_path.unwrap(), OpenOptions::default())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
assert!(validate_path_safety("/bad\0file.txt").is_err());
|
assert!(validate_path_safety("/bad\0file.txt").is_err());
|
||||||
|
|
||||||
// 2. Steuerzeichen < 0x20
|
// 2. Steuerzeichen < 0x20
|
||||||
@@ -1288,7 +1299,9 @@ mod tests {
|
|||||||
|
|
||||||
// 1. Schreibe 500 Bytes
|
// 1. Schreibe 500 Bytes
|
||||||
let payload = Bytes::from(vec![42u8; 500]);
|
let payload = Bytes::from(vec![42u8; 500]);
|
||||||
file.write_buf(Box::new(std::io::Cursor::new(payload))).await.unwrap();
|
file.write_buf(Box::new(std::io::Cursor::new(payload)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// 2. Expliziter Flush: muss Chunk & Dateigröße atomar persistieren
|
// 2. Expliziter Flush: muss Chunk & Dateigröße atomar persistieren
|
||||||
file.flush().await.unwrap();
|
file.flush().await.unwrap();
|
||||||
|
|||||||
+50
-45
@@ -44,7 +44,9 @@ pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
|||||||
Command::new("explorer.exe")
|
Command::new("explorer.exe")
|
||||||
.arg(&drive_path)
|
.arg(&drive_path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
|
.with_context(|| {
|
||||||
|
format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path)
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -73,15 +75,19 @@ pub fn open_in_file_manager(path: &std::path::Path) -> Result<()> {
|
|||||||
Command::new("explorer.exe")
|
Command::new("explorer.exe")
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", path.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Konnte Windows Explorer für '{}' nicht öffnen",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
Command::new("open")
|
Command::new("open").arg(path).spawn().with_context(|| {
|
||||||
.arg(path)
|
format!("Konnte macOS Finder für '{}' nicht öffnen", path.display())
|
||||||
.spawn()
|
})?;
|
||||||
.with_context(|| format!("Konnte macOS Finder für '{}' nicht öffnen", path.display()))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
@@ -89,7 +95,12 @@ pub fn open_in_file_manager(path: &std::path::Path) -> Result<()> {
|
|||||||
Command::new("xdg-open")
|
Command::new("xdg-open")
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.with_context(|| format!("Konnte Dateimanager via xdg-open für '{}' nicht öffnen", path.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Konnte Dateimanager via xdg-open für '{}' nicht öffnen",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(not(any(windows, unix)))]
|
#[cfg(not(any(windows, unix)))]
|
||||||
@@ -129,8 +140,7 @@ pub fn notify_shell_associations_changed() {
|
|||||||
let _ = Command::new("update-desktop-database").spawn();
|
let _ = Command::new("update-desktop-database").spawn();
|
||||||
}
|
}
|
||||||
#[cfg(not(any(windows, all(unix, not(target_os = "macos")))))]
|
#[cfg(not(any(windows, all(unix, not(target_os = "macos")))))]
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer bzw. unter Linux via Freedesktop.
|
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer bzw. unter Linux via Freedesktop.
|
||||||
@@ -143,11 +153,7 @@ pub fn register_explorer_integration() -> Result<()> {
|
|||||||
|
|
||||||
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", "", "Sanctum.Container"),
|
||||||
r"HKCU\Software\Classes\.sanctum",
|
|
||||||
"",
|
|
||||||
"Sanctum.Container",
|
|
||||||
),
|
|
||||||
// 2. ProgID Metadaten & Beschreibung
|
// 2. ProgID Metadaten & Beschreibung
|
||||||
(
|
(
|
||||||
r"HKCU\Software\Classes\Sanctum.Container",
|
r"HKCU\Software\Classes\Sanctum.Container",
|
||||||
@@ -205,7 +211,9 @@ pub fn register_explorer_integration() -> Result<()> {
|
|||||||
}
|
}
|
||||||
cmd.arg("/d").arg(val_data).arg("/f");
|
cmd.arg("/d").arg(val_data).arg("/f");
|
||||||
|
|
||||||
let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?;
|
let output = cmd
|
||||||
|
.output()
|
||||||
|
.with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
|
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
|
||||||
@@ -246,9 +254,7 @@ pub fn unregister_explorer_integration() -> Result<()> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for key in keys_to_delete {
|
for key in keys_to_delete {
|
||||||
let _ = Command::new("reg")
|
let _ = Command::new("reg").args(["delete", key, "/f"]).output();
|
||||||
.args(["delete", key, "/f"])
|
|
||||||
.output();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_shell_associations_changed();
|
notify_shell_associations_changed();
|
||||||
@@ -258,8 +264,11 @@ pub fn unregister_explorer_integration() -> Result<()> {
|
|||||||
{
|
{
|
||||||
if let Some(home) = std::env::var_os("HOME") {
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
let home_path = std::path::PathBuf::from(home);
|
let home_path = std::path::PathBuf::from(home);
|
||||||
let _ = std::fs::remove_file(home_path.join(".local/share/applications/sanctum.desktop"));
|
let _ =
|
||||||
let _ = std::fs::remove_file(home_path.join(".local/share/mime/packages/application-x-sanctum.xml"));
|
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();
|
notify_shell_associations_changed();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -496,9 +505,9 @@ pub fn start_session_lock_monitor(
|
|||||||
})
|
})
|
||||||
.context("Konnte Windows Session-Monitor-Thread nicht starten")?;
|
.context("Konnte Windows Session-Monitor-Thread nicht starten")?;
|
||||||
|
|
||||||
let hwnd = hwnd_rx
|
let hwnd = hwnd_rx.recv().map_err(|e| {
|
||||||
.recv()
|
anyhow::anyhow!("Session-Monitor-Thread initialisierte nicht rechtzeitig: {e}")
|
||||||
.map_err(|e| anyhow::anyhow!("Session-Monitor-Thread initialisierte nicht rechtzeitig: {e}"))?;
|
})?;
|
||||||
|
|
||||||
if hwnd == 0 {
|
if hwnd == 0 {
|
||||||
bail!("Win32-Nachrichtenfenster für Session-Lock konnte nicht erstellt werden");
|
bail!("Win32-Nachrichtenfenster für Session-Lock konnte nicht erstellt werden");
|
||||||
@@ -522,7 +531,8 @@ pub fn start_session_lock_monitor(
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static CONSOLE_CTRL_TX: std::sync::Mutex<Option<tokio::sync::mpsc::Sender<()>>> = std::sync::Mutex::new(None);
|
static CONSOLE_CTRL_TX: std::sync::Mutex<Option<tokio::sync::mpsc::Sender<()>>> =
|
||||||
|
std::sync::Mutex::new(None);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static CONSOLE_CTRL_DRIVE: std::sync::Mutex<Option<char>> = std::sync::Mutex::new(None);
|
static CONSOLE_CTRL_DRIVE: std::sync::Mutex<Option<char>> = std::sync::Mutex::new(None);
|
||||||
|
|
||||||
@@ -550,11 +560,7 @@ extern "system" {
|
|||||||
dwFlags: u32,
|
dwFlags: u32,
|
||||||
) -> u32;
|
) -> u32;
|
||||||
|
|
||||||
fn WNetCancelConnection2W(
|
fn WNetCancelConnection2W(lpName: *const u16, dwFlags: u32, fForce: i32) -> u32;
|
||||||
lpName: *const u16,
|
|
||||||
dwFlags: u32,
|
|
||||||
fForce: i32,
|
|
||||||
) -> u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -571,10 +577,8 @@ unsafe extern "system" fn console_ctrl_routine(ctrl_type: u32) -> i32 {
|
|||||||
if let Ok(guard) = CONSOLE_CTRL_DRIVE.lock() {
|
if let Ok(guard) = CONSOLE_CTRL_DRIVE.lock() {
|
||||||
if let Some(dl) = *guard {
|
if let Some(dl) = *guard {
|
||||||
let drive_str = format!("{}:", dl.to_ascii_uppercase());
|
let drive_str = format!("{}:", dl.to_ascii_uppercase());
|
||||||
let wide_drive: Vec<u16> = drive_str
|
let wide_drive: Vec<u16> =
|
||||||
.encode_utf16()
|
drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
.chain(std::iter::once(0))
|
|
||||||
.collect();
|
|
||||||
unsafe {
|
unsafe {
|
||||||
WNetCancelConnection2W(wide_drive.as_ptr(), 0, 1);
|
WNetCancelConnection2W(wide_drive.as_ptr(), 0, 1);
|
||||||
}
|
}
|
||||||
@@ -724,9 +728,15 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
|
|||||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
||||||
let mut local_name: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
let mut local_name: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
let remote_url = format!("http://127.0.0.1:{}/", port);
|
let remote_url = format!("http://127.0.0.1:{}/", port);
|
||||||
let mut remote_name: Vec<u16> = remote_url.encode_utf16().chain(std::iter::once(0)).collect();
|
let mut remote_name: Vec<u16> = remote_url
|
||||||
|
.encode_utf16()
|
||||||
|
.chain(std::iter::once(0))
|
||||||
|
.collect();
|
||||||
let username: Vec<u16> = "sanctum".encode_utf16().chain(std::iter::once(0)).collect();
|
let username: Vec<u16> = "sanctum".encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
let password: Vec<u16> = session_token.encode_utf16().chain(std::iter::once(0)).collect();
|
let password: Vec<u16> = session_token
|
||||||
|
.encode_utf16()
|
||||||
|
.chain(std::iter::once(0))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let nr = NETRESOURCEW {
|
let nr = NETRESOURCEW {
|
||||||
dwScope: 0,
|
dwScope: 0,
|
||||||
@@ -750,18 +760,14 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
|
|||||||
|
|
||||||
// PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85 / 1202)
|
// PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85 / 1202)
|
||||||
if res == 85 || res == 1202 {
|
if res == 85 || res == 1202 {
|
||||||
debug!("Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...", drive_str);
|
debug!(
|
||||||
|
"Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...",
|
||||||
|
drive_str
|
||||||
|
);
|
||||||
unsafe {
|
unsafe {
|
||||||
WNetCancelConnection2W(local_name.as_ptr(), 0, 1);
|
WNetCancelConnection2W(local_name.as_ptr(), 0, 1);
|
||||||
}
|
}
|
||||||
res = unsafe {
|
res = unsafe { WNetAddConnection2W(&nr, password.as_ptr(), username.as_ptr(), 0) };
|
||||||
WNetAddConnection2W(
|
|
||||||
&nr,
|
|
||||||
password.as_ptr(),
|
|
||||||
username.as_ptr(),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if res != 0 {
|
if res != 0 {
|
||||||
@@ -849,4 +855,3 @@ mod tests {
|
|||||||
drop(monitor);
|
drop(monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+140
-35
@@ -7,8 +7,8 @@ use rand::rngs::OsRng;
|
|||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
|
||||||
use sanctum::crypto::{
|
use sanctum::crypto::{
|
||||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
derive_kek, generate_dek, generate_salt, wrap_slot0_payload, wrap_slot1_payload, KdfParams,
|
||||||
wrap_slot1_payload, KdfParams, CHUNK_SIZE, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
CHUNK_SIZE, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||||
};
|
};
|
||||||
use sanctum::storage::Database;
|
use sanctum::storage::Database;
|
||||||
use sanctum::verify::verify_container;
|
use sanctum::verify::verify_container;
|
||||||
@@ -45,8 +45,7 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
let dek_1 = generate_dek();
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
let carrier_node_id = 3i64;
|
let carrier_node_id = 3i64;
|
||||||
let (wrapped_0, nonce_0, tag_0) =
|
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
|
||||||
let (wrapped_1, nonce_1, tag_1) =
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
|
|
||||||
@@ -103,7 +102,10 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
let carrier_path = DavPath::new("/system_backup.dat").unwrap();
|
let carrier_path = DavPath::new("/system_backup.dat").unwrap();
|
||||||
|
|
||||||
// Metadaten der Alibi-Datei im Decoy prüfen
|
// Metadaten der Alibi-Datei im Decoy prüfen
|
||||||
let carrier_meta = decoy_fs.metadata(&carrier_path).await.expect("Carrier meta");
|
let carrier_meta = decoy_fs
|
||||||
|
.metadata(&carrier_path)
|
||||||
|
.await
|
||||||
|
.expect("Carrier meta");
|
||||||
assert_eq!(carrier_meta.len(), carrier_size_bytes);
|
assert_eq!(carrier_meta.len(), carrier_size_bytes);
|
||||||
assert!(!carrier_meta.is_dir());
|
assert!(!carrier_meta.is_dir());
|
||||||
|
|
||||||
@@ -113,20 +115,29 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
matches!(decoy_fs.open(&carrier_path, write_opts).await, Err(FsError::Forbidden)),
|
matches!(
|
||||||
|
decoy_fs.open(&carrier_path, write_opts).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
),
|
||||||
"Alibi-Datei darf nicht zum Schreiben geöffnet werden"
|
"Alibi-Datei darf nicht zum Schreiben geöffnet werden"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Alibi-Datei darf im Decoy-Mount NICHT gelöscht werden
|
// Alibi-Datei darf im Decoy-Mount NICHT gelöscht werden
|
||||||
assert!(
|
assert!(
|
||||||
matches!(decoy_fs.remove_file(&carrier_path).await, Err(FsError::Forbidden)),
|
matches!(
|
||||||
|
decoy_fs.remove_file(&carrier_path).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
),
|
||||||
"Alibi-Datei darf nicht gelöscht werden"
|
"Alibi-Datei darf nicht gelöscht werden"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Alibi-Datei darf im Decoy-Mount NICHT umbenannt werden
|
// Alibi-Datei darf im Decoy-Mount NICHT umbenannt werden
|
||||||
let new_name = DavPath::new("/renamed.iso").unwrap();
|
let new_name = DavPath::new("/renamed.iso").unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(decoy_fs.rename(&carrier_path, &new_name).await, Err(FsError::Forbidden)),
|
matches!(
|
||||||
|
decoy_fs.rename(&carrier_path, &new_name).await,
|
||||||
|
Err(FsError::Forbidden)
|
||||||
|
),
|
||||||
"Alibi-Datei darf nicht umbenannt werden"
|
"Alibi-Datei darf nicht umbenannt werden"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -135,8 +146,14 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
read: true,
|
read: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut file_reader = decoy_fs.open(&carrier_path, read_opts).await.expect("Open read");
|
let mut file_reader = decoy_fs
|
||||||
let first_mb = file_reader.read_bytes(CHUNK_SIZE).await.expect("Read first chunk");
|
.open(&carrier_path, read_opts)
|
||||||
|
.await
|
||||||
|
.expect("Open read");
|
||||||
|
let first_mb = file_reader
|
||||||
|
.read_bytes(CHUNK_SIZE)
|
||||||
|
.await
|
||||||
|
.expect("Read first chunk");
|
||||||
assert_eq!(first_mb.len(), CHUNK_SIZE);
|
assert_eq!(first_mb.len(), CHUNK_SIZE);
|
||||||
|
|
||||||
// 3. Hidden Mount: Dateisystem-Operationen innerhalb des Alibi-Carriers
|
// 3. Hidden Mount: Dateisystem-Operationen innerhalb des Alibi-Carriers
|
||||||
@@ -161,11 +178,17 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
entries.push(item.unwrap().name());
|
entries.push(item.unwrap().name());
|
||||||
}
|
}
|
||||||
assert!(entries.is_empty(), "Hidden Vault Wurzelverzeichnis muss anfangs leer sein");
|
assert!(
|
||||||
|
entries.is_empty(),
|
||||||
|
"Hidden Vault Wurzelverzeichnis muss anfangs leer sein"
|
||||||
|
);
|
||||||
|
|
||||||
// Ordner erstellen
|
// Ordner erstellen
|
||||||
let secret_dir = DavPath::new("/Classified").unwrap();
|
let secret_dir = DavPath::new("/Classified").unwrap();
|
||||||
hidden_fs.create_dir(&secret_dir).await.expect("Create Classified dir");
|
hidden_fs
|
||||||
|
.create_dir(&secret_dir)
|
||||||
|
.await
|
||||||
|
.expect("Create Classified dir");
|
||||||
|
|
||||||
// Datei im Ordner anlegen und schreiben
|
// Datei im Ordner anlegen und schreiben
|
||||||
let secret_file_path = DavPath::new("/Classified/passwords.txt").unwrap();
|
let secret_file_path = DavPath::new("/Classified/passwords.txt").unwrap();
|
||||||
@@ -196,14 +219,24 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
.open(&secret_file_path, read_opts)
|
.open(&secret_file_path, read_opts)
|
||||||
.await
|
.await
|
||||||
.expect("Open secret file for read");
|
.expect("Open secret file for read");
|
||||||
let read_data = read_handle.read_bytes(1024).await.expect("Read secret bytes");
|
let read_data = read_handle
|
||||||
|
.read_bytes(1024)
|
||||||
|
.await
|
||||||
|
.expect("Read secret bytes");
|
||||||
assert_eq!(&read_data[..], secret_content);
|
assert_eq!(&read_data[..], secret_content);
|
||||||
drop(read_handle);
|
drop(read_handle);
|
||||||
|
|
||||||
// Größere Binärdatei schreiben (über 2 MB = 2 Blöcke)
|
// Größere Binärdatei schreiben (über 2 MB = 2 Blöcke)
|
||||||
let big_file_path = DavPath::new("/Classified/payload.bin").unwrap();
|
let big_file_path = DavPath::new("/Classified/payload.bin").unwrap();
|
||||||
let mut big_file = hidden_fs
|
let mut big_file = hidden_fs
|
||||||
.open(&big_file_path, OpenOptions { create: true, write: true, ..Default::default() })
|
.open(
|
||||||
|
&big_file_path,
|
||||||
|
OpenOptions {
|
||||||
|
create: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Create big file");
|
.expect("Create big file");
|
||||||
|
|
||||||
@@ -211,28 +244,46 @@ async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
|||||||
let mut payload = vec![0u8; payload_size];
|
let mut payload = vec![0u8; payload_size];
|
||||||
OsRng.fill_bytes(&mut payload);
|
OsRng.fill_bytes(&mut payload);
|
||||||
|
|
||||||
big_file.write_bytes(Bytes::copy_from_slice(&payload)).await.expect("Write big payload");
|
big_file
|
||||||
|
.write_bytes(Bytes::copy_from_slice(&payload))
|
||||||
|
.await
|
||||||
|
.expect("Write big payload");
|
||||||
big_file.flush().await.expect("Flush big file");
|
big_file.flush().await.expect("Flush big file");
|
||||||
drop(big_file);
|
drop(big_file);
|
||||||
|
|
||||||
// Datei zurücklesen und Bit-für-Bit verifizieren
|
// Datei zurücklesen und Bit-für-Bit verifizieren
|
||||||
let mut read_big = hidden_fs
|
let mut read_big = hidden_fs
|
||||||
.open(&big_file_path, OpenOptions { read: true, ..Default::default() })
|
.open(
|
||||||
|
&big_file_path,
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Open big file");
|
.expect("Open big file");
|
||||||
let read_big_bytes = read_big.read_bytes(payload_size + 100).await.expect("Read big file bytes");
|
let read_big_bytes = read_big
|
||||||
|
.read_bytes(payload_size + 100)
|
||||||
|
.await
|
||||||
|
.expect("Read big file bytes");
|
||||||
assert_eq!(read_big_bytes.len(), payload_size);
|
assert_eq!(read_big_bytes.len(), payload_size);
|
||||||
assert_eq!(&read_big_bytes[..], &payload[..]);
|
assert_eq!(&read_big_bytes[..], &payload[..]);
|
||||||
drop(read_big);
|
drop(read_big);
|
||||||
|
|
||||||
// Datei umbenennen
|
// Datei umbenennen
|
||||||
let renamed_path = DavPath::new("/Classified/renamed_payload.bin").unwrap();
|
let renamed_path = DavPath::new("/Classified/renamed_payload.bin").unwrap();
|
||||||
hidden_fs.rename(&big_file_path, &renamed_path).await.expect("Rename file");
|
hidden_fs
|
||||||
|
.rename(&big_file_path, &renamed_path)
|
||||||
|
.await
|
||||||
|
.expect("Rename file");
|
||||||
assert!(hidden_fs.metadata(&big_file_path).await.is_err());
|
assert!(hidden_fs.metadata(&big_file_path).await.is_err());
|
||||||
assert!(hidden_fs.metadata(&renamed_path).await.is_ok());
|
assert!(hidden_fs.metadata(&renamed_path).await.is_ok());
|
||||||
|
|
||||||
// Datei löschen (Blöcke werden geshreddert und freigegeben)
|
// Datei löschen (Blöcke werden geshreddert und freigegeben)
|
||||||
hidden_fs.remove_file(&renamed_path).await.expect("Remove file");
|
hidden_fs
|
||||||
|
.remove_file(&renamed_path)
|
||||||
|
.await
|
||||||
|
.expect("Remove file");
|
||||||
assert!(hidden_fs.metadata(&renamed_path).await.is_err());
|
assert!(hidden_fs.metadata(&renamed_path).await.is_err());
|
||||||
|
|
||||||
// Checkpoint SQLite
|
// Checkpoint SQLite
|
||||||
@@ -284,8 +335,7 @@ async fn test_model_a_container_file_size_invariance() {
|
|||||||
let dek_1 = generate_dek();
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
let carrier_node_id = 3i64;
|
let carrier_node_id = 3i64;
|
||||||
let (wrapped_0, nonce_0, tag_0) =
|
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
|
||||||
let (wrapped_1, nonce_1, tag_1) =
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
|
|
||||||
@@ -315,7 +365,10 @@ async fn test_model_a_container_file_size_invariance() {
|
|||||||
|
|
||||||
// Initiale Dateigröße messen
|
// Initiale Dateigröße messen
|
||||||
let initial_file_size = std::fs::metadata(&path).unwrap().len();
|
let initial_file_size = std::fs::metadata(&path).unwrap().len();
|
||||||
assert!(initial_file_size >= carrier_size_bytes, "Containergröße muss mindestens 10 MB betragen");
|
assert!(
|
||||||
|
initial_file_size >= carrier_size_bytes,
|
||||||
|
"Containergröße muss mindestens 10 MB betragen"
|
||||||
|
);
|
||||||
|
|
||||||
// Hidden Mount öffnen und 4 MB geheime Daten schreiben
|
// Hidden Mount öffnen und 4 MB geheime Daten schreiben
|
||||||
let meta = db.read_meta().unwrap();
|
let meta = db.read_meta().unwrap();
|
||||||
@@ -333,13 +386,23 @@ async fn test_model_a_container_file_size_invariance() {
|
|||||||
|
|
||||||
let test_file = DavPath::new("/large_confidential.pdf").unwrap();
|
let test_file = DavPath::new("/large_confidential.pdf").unwrap();
|
||||||
let mut handle = hidden_fs
|
let mut handle = hidden_fs
|
||||||
.open(&test_file, OpenOptions { create: true, write: true, ..Default::default() })
|
.open(
|
||||||
|
&test_file,
|
||||||
|
OpenOptions {
|
||||||
|
create: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Open file");
|
.expect("Open file");
|
||||||
|
|
||||||
let mut random_data = vec![0u8; 4 * 1024 * 1024]; // 4 MB
|
let mut random_data = vec![0u8; 4 * 1024 * 1024]; // 4 MB
|
||||||
OsRng.fill_bytes(&mut random_data);
|
OsRng.fill_bytes(&mut random_data);
|
||||||
handle.write_bytes(Bytes::copy_from_slice(&random_data)).await.expect("Write 4MB");
|
handle
|
||||||
|
.write_bytes(Bytes::copy_from_slice(&random_data))
|
||||||
|
.await
|
||||||
|
.expect("Write 4MB");
|
||||||
handle.flush().await.expect("Flush 4MB");
|
handle.flush().await.expect("Flush 4MB");
|
||||||
drop(handle);
|
drop(handle);
|
||||||
|
|
||||||
@@ -383,8 +446,7 @@ async fn test_carrier_file_drop_and_append_mode() {
|
|||||||
let dek_1 = generate_dek();
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
let carrier_node_id = 3i64;
|
let carrier_node_id = 3i64;
|
||||||
let (wrapped_0, nonce_0, tag_0) =
|
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
|
||||||
let (wrapped_1, nonce_1, tag_1) =
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
|
|
||||||
@@ -428,44 +490,87 @@ async fn test_carrier_file_drop_and_append_mode() {
|
|||||||
// 1. TEST CarrierFile::drop: Write bytes OHNE expliziten flush(), dann drop(handle)
|
// 1. TEST CarrierFile::drop: Write bytes OHNE expliziten flush(), dann drop(handle)
|
||||||
let test_file = DavPath::new("/drop_flush_test.txt").unwrap();
|
let test_file = DavPath::new("/drop_flush_test.txt").unwrap();
|
||||||
let mut write_handle = hidden_fs
|
let mut write_handle = hidden_fs
|
||||||
.open(&test_file, OpenOptions { create: true, write: true, ..Default::default() })
|
.open(
|
||||||
|
&test_file,
|
||||||
|
OpenOptions {
|
||||||
|
create: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Open file for write");
|
.expect("Open file for write");
|
||||||
|
|
||||||
let initial_data = b"Hello from unflushed write!";
|
let initial_data = b"Hello from unflushed write!";
|
||||||
write_handle.write_bytes(Bytes::from_static(initial_data)).await.expect("Write initial data");
|
write_handle
|
||||||
|
.write_bytes(Bytes::from_static(initial_data))
|
||||||
|
.await
|
||||||
|
.expect("Write initial data");
|
||||||
// WICHTIG: KEIN write_handle.flush()! Nur drop:
|
// WICHTIG: KEIN write_handle.flush()! Nur drop:
|
||||||
drop(write_handle);
|
drop(write_handle);
|
||||||
|
|
||||||
// Jetzt Datei wieder lesend öffnen und prüfen, ob Daten durch Drop persistiert wurden
|
// Jetzt Datei wieder lesend öffnen und prüfen, ob Daten durch Drop persistiert wurden
|
||||||
let mut read_handle = hidden_fs
|
let mut read_handle = hidden_fs
|
||||||
.open(&test_file, OpenOptions { read: true, ..Default::default() })
|
.open(
|
||||||
|
&test_file,
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Open file for read");
|
.expect("Open file for read");
|
||||||
let read_back = read_handle.read_bytes(100).await.expect("Read data back");
|
let read_back = read_handle.read_bytes(100).await.expect("Read data back");
|
||||||
assert_eq!(&read_back[..], initial_data, "Drop muss ungeflushte Datenblöcke und Inode automatisch sichern");
|
assert_eq!(
|
||||||
|
&read_back[..],
|
||||||
|
initial_data,
|
||||||
|
"Drop muss ungeflushte Datenblöcke und Inode automatisch sichern"
|
||||||
|
);
|
||||||
drop(read_handle);
|
drop(read_handle);
|
||||||
|
|
||||||
// 2. TEST O_APPEND: Im Append-Modus öffnen und weitere Daten anhängen
|
// 2. TEST O_APPEND: Im Append-Modus öffnen und weitere Daten anhängen
|
||||||
let append_data = b" - Appended data at EOF!";
|
let append_data = b" - Appended data at EOF!";
|
||||||
let mut append_handle = hidden_fs
|
let mut append_handle = hidden_fs
|
||||||
.open(&test_file, OpenOptions { write: true, append: true, ..Default::default() })
|
.open(
|
||||||
|
&test_file,
|
||||||
|
OpenOptions {
|
||||||
|
write: true,
|
||||||
|
append: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Open file for append");
|
.expect("Open file for append");
|
||||||
|
|
||||||
append_handle.write_bytes(Bytes::from_static(append_data)).await.expect("Write appended data");
|
append_handle
|
||||||
|
.write_bytes(Bytes::from_static(append_data))
|
||||||
|
.await
|
||||||
|
.expect("Write appended data");
|
||||||
drop(append_handle); // Drop sichert auch hier
|
drop(append_handle); // Drop sichert auch hier
|
||||||
|
|
||||||
// Prüfe den vollständigen Dateiinhalt nach Append
|
// Prüfe den vollständigen Dateiinhalt nach Append
|
||||||
let mut read_handle_2 = hidden_fs
|
let mut read_handle_2 = hidden_fs
|
||||||
.open(&test_file, OpenOptions { read: true, ..Default::default() })
|
.open(
|
||||||
|
&test_file,
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Open file for read after append");
|
.expect("Open file for read after append");
|
||||||
let full_content = read_handle_2.read_bytes(200).await.expect("Read full content");
|
let full_content = read_handle_2
|
||||||
|
.read_bytes(200)
|
||||||
|
.await
|
||||||
|
.expect("Read full content");
|
||||||
let mut expected = Vec::new();
|
let mut expected = Vec::new();
|
||||||
expected.extend_from_slice(initial_data);
|
expected.extend_from_slice(initial_data);
|
||||||
expected.extend_from_slice(append_data);
|
expected.extend_from_slice(append_data);
|
||||||
assert_eq!(&full_content[..], &expected[..], "O_APPEND muss Daten am Dateiende anhängen");
|
assert_eq!(
|
||||||
|
&full_content[..],
|
||||||
|
&expected[..],
|
||||||
|
"O_APPEND muss Daten am Dateiende anhängen"
|
||||||
|
);
|
||||||
drop(read_handle_2);
|
drop(read_handle_2);
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|||||||
+506
-132
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,10 @@ use sanctum::{
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_live_crash_and_recovery_stress() {
|
async fn test_live_crash_and_recovery_stress() {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let container_path: PathBuf = temp_dir.join(format!("sanctum_live_stress_{}.sanctum", std::process::id()));
|
let container_path: PathBuf = temp_dir.join(format!(
|
||||||
|
"sanctum_live_stress_{}.sanctum",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
if container_path.exists() {
|
if container_path.exists() {
|
||||||
let _ = std::fs::remove_file(&container_path);
|
let _ = std::fs::remove_file(&container_path);
|
||||||
}
|
}
|
||||||
@@ -105,13 +108,17 @@ async fn test_live_crash_and_recovery_stress() {
|
|||||||
|
|
||||||
// 4. Recovery & Integritätsprüfung nach Crash
|
// 4. Recovery & Integritätsprüfung nach Crash
|
||||||
// Das System muss die SQLite WAL-Datei automatisch erkennen und verarbeiten
|
// 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");
|
let verify_result =
|
||||||
|
verify_container(&container_path, Some(&dek), false).expect("Verify post-crash");
|
||||||
assert!(
|
assert!(
|
||||||
verify_result.is_healthy(),
|
verify_result.is_healthy(),
|
||||||
"Container muss nach Crash vollkommen konsistent sein! Fehler: {:?}",
|
"Container muss nach Crash vollkommen konsistent sein! Fehler: {:?}",
|
||||||
verify_result.errors
|
verify_result.errors
|
||||||
);
|
);
|
||||||
assert_eq!(verify_result.corrupted_chunks, 0, "Keine korrupten Chunks erlaubt");
|
assert_eq!(
|
||||||
|
verify_result.corrupted_chunks, 0,
|
||||||
|
"Keine korrupten Chunks erlaubt"
|
||||||
|
);
|
||||||
|
|
||||||
// 5. Konsistentes Weiterarbeiten nach dem Absturz
|
// 5. Konsistentes Weiterarbeiten nach dem Absturz
|
||||||
let db_recovered = Database::open(&container_path).expect("Open database after crash");
|
let db_recovered = Database::open(&container_path).expect("Open database after crash");
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use sanctum::crypto::{
|
use sanctum::crypto::{
|
||||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
||||||
MIN_TIME_COST,
|
MIN_TIME_COST,
|
||||||
};
|
};
|
||||||
use sanctum::mount::is_loopback_host;
|
use sanctum::mount::is_loopback_host;
|
||||||
use sanctum::storage::Database;
|
use sanctum::storage::Database;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_loopback_host_comprehensive() {
|
fn test_is_loopback_host_comprehensive() {
|
||||||
@@ -45,7 +45,8 @@ fn test_is_loopback_host_comprehensive() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_wal_and_shm_cleanup_on_close() {
|
fn test_wal_and_shm_cleanup_on_close() {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let container_path: PathBuf = temp_dir.join(format!("test_wal_cleanup_{}.sanctum", std::process::id()));
|
let container_path: PathBuf =
|
||||||
|
temp_dir.join(format!("test_wal_cleanup_{}.sanctum", std::process::id()));
|
||||||
if container_path.exists() {
|
if container_path.exists() {
|
||||||
let _ = std::fs::remove_file(&container_path);
|
let _ = std::fs::remove_file(&container_path);
|
||||||
}
|
}
|
||||||
@@ -61,8 +62,9 @@ fn test_wal_and_shm_cleanup_on_close() {
|
|||||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&container_path).unwrap();
|
let db = Database::open(&container_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Erstelle Knoten, um Schreibaktivität im WAL zu erzeugen
|
// Erstelle Knoten, um Schreibaktivität im WAL zu erzeugen
|
||||||
let _ = db.create_node(1, "test_file.txt", false).unwrap();
|
let _ = db.create_node(1, "test_file.txt", false).unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
@@ -77,24 +79,33 @@ fn test_wal_and_shm_cleanup_on_close() {
|
|||||||
shm.push("-shm");
|
shm.push("-shm");
|
||||||
let _ = std::fs::remove_file(&shm);
|
let _ = std::fs::remove_file(&shm);
|
||||||
|
|
||||||
assert!(!PathBuf::from(wal).exists(), "WAL-Datei darf nach sauberem Unmount nicht zurückbleiben");
|
assert!(
|
||||||
assert!(!PathBuf::from(shm).exists(), "SHM-Datei darf nach sauberem Unmount nicht zurückbleiben");
|
!PathBuf::from(wal).exists(),
|
||||||
|
"WAL-Datei darf nach sauberem Unmount nicht zurückbleiben"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!PathBuf::from(shm).exists(),
|
||||||
|
"SHM-Datei darf nach sauberem Unmount nicht zurückbleiben"
|
||||||
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&container_path);
|
let _ = std::fs::remove_file(&container_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mount_security_multi_auth_and_no_token_in_url() {
|
fn test_mount_security_multi_auth_and_no_token_in_url() {
|
||||||
use sanctum::mount::{check_basic_auth, check_token_header, strip_path_prefix};
|
use sanctum::mount::{check_basic_auth, check_token_header, uri_contains_token};
|
||||||
|
|
||||||
let session_token = "4f8a12bc90de45f187a23456789abcde";
|
let session_token = "4f8a12bc90de45f187a23456789abcde";
|
||||||
let port = 8443;
|
let port = 8443;
|
||||||
let remote_url = format!("http://127.0.0.1:{}/", port);
|
let remote_url = format!("http://127.0.0.1:{}/", port);
|
||||||
|
|
||||||
// R-05: Die Remote-URL für Mount-Befehle darf niemals das Session-Token enthalten!
|
// R-05 & SA-05: Die Remote-URL für Mount-Befehle darf niemals das Session-Token enthalten!
|
||||||
assert!(!remote_url.contains(session_token), "Remote URL darf niemals das Session-Token enthalten");
|
assert!(
|
||||||
|
!remote_url.contains(session_token),
|
||||||
|
"Remote URL darf niemals das Session-Token enthalten"
|
||||||
|
);
|
||||||
|
|
||||||
// 1. Basic Auth Prüfung
|
// 1. Basic Auth Prüfung (Constant-Time)
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let auth_header = format!(
|
let auth_header = format!(
|
||||||
"Basic {}",
|
"Basic {}",
|
||||||
@@ -102,14 +113,44 @@ fn test_mount_security_multi_auth_and_no_token_in_url() {
|
|||||||
);
|
);
|
||||||
assert!(check_basic_auth(&auth_header, session_token));
|
assert!(check_basic_auth(&auth_header, session_token));
|
||||||
|
|
||||||
// 2. Token Header Prüfung
|
let wrong_auth = format!(
|
||||||
|
"Basic {}",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode("sanctum:wrong_token_1234567890abcdef")
|
||||||
|
);
|
||||||
|
assert!(!check_basic_auth(&wrong_auth, session_token));
|
||||||
|
|
||||||
|
// 2. Token Header Prüfung (Constant-Time)
|
||||||
let mut headers = hyper::HeaderMap::new();
|
let mut headers = hyper::HeaderMap::new();
|
||||||
headers.insert("X-Sanctum-Token", session_token.parse().unwrap());
|
headers.insert("X-Sanctum-Token", session_token.parse().unwrap());
|
||||||
assert!(check_token_header(&headers, session_token));
|
assert!(check_token_header(&headers, session_token));
|
||||||
|
|
||||||
// 3. Path-Prefix Fallback
|
let mut wrong_headers = hyper::HeaderMap::new();
|
||||||
let prefix = format!("/{}", session_token);
|
wrong_headers.insert(
|
||||||
let uri: hyper::Uri = format!("http://127.0.0.1:8443/{}/test.txt", session_token).parse().unwrap();
|
"X-Sanctum-Token",
|
||||||
let stripped = strip_path_prefix(&uri, &prefix).expect("Strip prefix");
|
"wrong_token_1234567890abcdef".parse().unwrap(),
|
||||||
assert_eq!(stripped.path(), "/test.txt");
|
);
|
||||||
|
assert!(!check_token_header(&wrong_headers, session_token));
|
||||||
|
|
||||||
|
// 3. SA-05: Verifikation, dass Token in URI (Pfad oder Query) erkannt und strikt abgewiesen wird
|
||||||
|
let uri_path: hyper::Uri = format!("http://127.0.0.1:8443/{}/test.txt", session_token)
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
uri_contains_token(&uri_path, session_token),
|
||||||
|
"Token im Pfad muss erkannt werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
let uri_query: hyper::Uri = format!("http://127.0.0.1:8443/test.txt?token={}", session_token)
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
uri_contains_token(&uri_query, session_token),
|
||||||
|
"Token im Query-String muss erkannt werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
let uri_clean: hyper::Uri = "http://127.0.0.1:8443/test.txt".parse().unwrap();
|
||||||
|
assert!(
|
||||||
|
!uri_contains_token(&uri_clean, session_token),
|
||||||
|
"Saubere URI darf kein Token enthalten"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,25 @@ fn test_validate_node_name_rejections() {
|
|||||||
|
|
||||||
// 6. Windows reservierte Gerätenamen
|
// 6. Windows reservierte Gerätenamen
|
||||||
let reserved_names = [
|
let reserved_names = [
|
||||||
"CON", "con", "prn", "PRN", "aux", "AUX", "nul", "NUL",
|
"CON",
|
||||||
"COM1", "com2", "COM9", "lpt1", "LPT2", "LPT9",
|
"con",
|
||||||
"con.txt", "aux.dat", "NUL.tar.gz", "com1.log", "prn.pdf",
|
"prn",
|
||||||
|
"PRN",
|
||||||
|
"aux",
|
||||||
|
"AUX",
|
||||||
|
"nul",
|
||||||
|
"NUL",
|
||||||
|
"COM1",
|
||||||
|
"com2",
|
||||||
|
"COM9",
|
||||||
|
"lpt1",
|
||||||
|
"LPT2",
|
||||||
|
"LPT9",
|
||||||
|
"con.txt",
|
||||||
|
"aux.dat",
|
||||||
|
"NUL.tar.gz",
|
||||||
|
"com1.log",
|
||||||
|
"prn.pdf",
|
||||||
];
|
];
|
||||||
for res in reserved_names {
|
for res in reserved_names {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -64,7 +80,10 @@ fn test_validate_node_name_accepted() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_storage_create_and_rename_reject_invalid_names() {
|
fn test_storage_create_and_rename_reject_invalid_names() {
|
||||||
let mut path = std::env::temp_dir();
|
let mut path = std::env::temp_dir();
|
||||||
path.push(format!("sanctum_test_pathutil_{}.sanctum", std::process::id()));
|
path.push(format!(
|
||||||
|
"sanctum_test_pathutil_{}.sanctum",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
@@ -80,20 +99,31 @@ fn test_storage_create_and_rename_reject_invalid_names() {
|
|||||||
let (wrapped, nonce, tag) = sanctum::crypto::wrap_dek(&kek, &dek).unwrap();
|
let (wrapped, nonce, tag) = sanctum::crypto::wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = sanctum::storage::Database::open(&path).unwrap();
|
let db = sanctum::storage::Database::open(&path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// create_node_in_vault mit ungültigem Namen muss scheitern
|
// create_node_in_vault mit ungültigem Namen muss scheitern
|
||||||
assert!(db.create_node_in_vault(0, 1, "..", false, &dek).is_err());
|
assert!(db.create_node_in_vault(0, 1, "..", false, &dek).is_err());
|
||||||
assert!(db.create_node_in_vault(0, 1, "CON", false, &dek).is_err());
|
assert!(db.create_node_in_vault(0, 1, "CON", false, &dek).is_err());
|
||||||
assert!(db.create_node_in_vault(0, 1, "sub/dir", false, &dek).is_err());
|
assert!(db
|
||||||
|
.create_node_in_vault(0, 1, "sub/dir", false, &dek)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
// Gültige Datei erstellen
|
// Gültige Datei erstellen
|
||||||
let valid_node = db.create_node_in_vault(0, 1, "valid.txt", false, &dek).unwrap();
|
let valid_node = db
|
||||||
|
.create_node_in_vault(0, 1, "valid.txt", false, &dek)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// rename_node_in_vault mit ungültigem Namen muss scheitern
|
// rename_node_in_vault mit ungültigem Namen muss scheitern
|
||||||
assert!(db.rename_node_in_vault(valid_node.id, 1, "..", 0, &dek).is_err());
|
assert!(db
|
||||||
assert!(db.rename_node_in_vault(valid_node.id, 1, "AUX", 0, &dek).is_err());
|
.rename_node_in_vault(valid_node.id, 1, "..", 0, &dek)
|
||||||
assert!(db.rename_node_in_vault(valid_node.id, 1, "bad\\name", 0, &dek).is_err());
|
.is_err());
|
||||||
|
assert!(db
|
||||||
|
.rename_node_in_vault(valid_node.id, 1, "AUX", 0, &dek)
|
||||||
|
.is_err());
|
||||||
|
assert!(db
|
||||||
|
.rename_node_in_vault(valid_node.id, 1, "bad\\name", 0, &dek)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use sanctum::crypto::{
|
use sanctum::crypto::{
|
||||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
derive_kek, generate_dek, generate_salt, wrap_slot0_payload, wrap_slot1_payload, KdfParams,
|
||||||
wrap_slot1_payload, KdfParams, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
|
||||||
};
|
};
|
||||||
use sanctum::storage::Database;
|
use sanctum::storage::Database;
|
||||||
use sanctum::sync::{delete_orphans_in_vault, SyncStats};
|
use sanctum::sync::{delete_orphans_in_vault, SyncStats};
|
||||||
@@ -41,8 +41,7 @@ fn test_carrier_protection_in_storage_and_sync() {
|
|||||||
let dek_1 = generate_dek();
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
let carrier_node_id = 3i64;
|
let carrier_node_id = 3i64;
|
||||||
let (wrapped_0, nonce_0, tag_0) =
|
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
|
||||||
let (wrapped_1, nonce_1, tag_1) =
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
|
|
||||||
@@ -74,18 +73,33 @@ fn test_carrier_protection_in_storage_and_sync() {
|
|||||||
|
|
||||||
// 1. Direkter Löschversuch der Trägerdatei muss fail-closed abgewehrt werden
|
// 1. Direkter Löschversuch der Trägerdatei muss fail-closed abgewehrt werden
|
||||||
let del_res = db.delete_node(carrier_node_id);
|
let del_res = db.delete_node(carrier_node_id);
|
||||||
assert!(del_res.is_err(), "Löschen der Trägerdatei muss fehlschlagen");
|
assert!(
|
||||||
|
del_res.is_err(),
|
||||||
|
"Löschen der Trägerdatei muss fehlschlagen"
|
||||||
|
);
|
||||||
assert!(del_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
assert!(del_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
||||||
|
|
||||||
// 2. Umbenennung der Trägerdatei muss fail-closed abgewehrt werden
|
// 2. Umbenennung der Trägerdatei muss fail-closed abgewehrt werden
|
||||||
let rename_res = db.rename_node_in_vault(carrier_node_id, 1, "renamed.iso", 0, &dek_0);
|
let rename_res = db.rename_node_in_vault(carrier_node_id, 1, "renamed.iso", 0, &dek_0);
|
||||||
assert!(rename_res.is_err(), "Umbenennen der Trägerdatei muss fehlschlagen");
|
assert!(
|
||||||
assert!(rename_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
rename_res.is_err(),
|
||||||
|
"Umbenennen der Trägerdatei muss fehlschlagen"
|
||||||
|
);
|
||||||
|
assert!(rename_res
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("Carrier-Schutz"));
|
||||||
|
|
||||||
// 3. Truncate der Trägerdatei muss fail-closed abgewehrt werden
|
// 3. Truncate der Trägerdatei muss fail-closed abgewehrt werden
|
||||||
let trunc_res = db.truncate_chunks_after(carrier_node_id, 0);
|
let trunc_res = db.truncate_chunks_after(carrier_node_id, 0);
|
||||||
assert!(trunc_res.is_err(), "Truncate der Trägerdatei muss fehlschlagen");
|
assert!(
|
||||||
assert!(trunc_res.unwrap_err().to_string().contains("Carrier-Schutz"));
|
trunc_res.is_err(),
|
||||||
|
"Truncate der Trägerdatei muss fehlschlagen"
|
||||||
|
);
|
||||||
|
assert!(trunc_res
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("Carrier-Schutz"));
|
||||||
|
|
||||||
// 4. delete_orphans_in_vault mit leerem lokalem Pfad-Set:
|
// 4. delete_orphans_in_vault mit leerem lokalem Pfad-Set:
|
||||||
// Der Carrier darf unter keinen Umständen gelöscht werden!
|
// Der Carrier darf unter keinen Umständen gelöscht werden!
|
||||||
@@ -102,16 +116,26 @@ fn test_carrier_protection_in_storage_and_sync() {
|
|||||||
true, // quiet
|
true, // quiet
|
||||||
&mut stats,
|
&mut stats,
|
||||||
);
|
);
|
||||||
assert!(orphan_res.is_ok(), "delete_orphans_in_vault muss ohne Fehler durchlaufen");
|
assert!(
|
||||||
assert_eq!(stats.files_deleted, 0, "Carrier-Datei darf nicht gelöscht worden sein");
|
orphan_res.is_ok(),
|
||||||
|
"delete_orphans_in_vault muss ohne Fehler durchlaufen"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stats.files_deleted, 0,
|
||||||
|
"Carrier-Datei darf nicht gelöscht worden sein"
|
||||||
|
);
|
||||||
|
|
||||||
// Sicherstellen, dass Carrier nach wie vor in der DB existiert
|
// Sicherstellen, dass Carrier nach wie vor in der DB existiert
|
||||||
let carrier_rec = db.get_node_by_id(carrier_node_id).unwrap();
|
let carrier_rec = db.get_node_by_id(carrier_node_id).unwrap();
|
||||||
assert!(carrier_rec.is_some(), "Carrier-Knoten muss nach Sync unversehrt vorhanden sein");
|
assert!(
|
||||||
|
carrier_rec.is_some(),
|
||||||
|
"Carrier-Knoten muss nach Sync unversehrt vorhanden sein"
|
||||||
|
);
|
||||||
|
|
||||||
// 5. R-02: sync_single_file_to_host muss Carrier-Pull strikt ablehnen
|
// 5. R-02: sync_single_file_to_host muss Carrier-Pull strikt ablehnen
|
||||||
let carrier_node = db.get_node_by_id(carrier_node_id).unwrap().unwrap();
|
let carrier_node = db.get_node_by_id(carrier_node_id).unwrap().unwrap();
|
||||||
let local_dest_file = std::env::temp_dir().join(format!("carrier_leak_{}.iso", std::process::id()));
|
let local_dest_file =
|
||||||
|
std::env::temp_dir().join(format!("carrier_leak_{}.iso", std::process::id()));
|
||||||
let pull_single_res = sanctum::sync::sync_single_file_to_host(
|
let pull_single_res = sanctum::sync::sync_single_file_to_host(
|
||||||
&db,
|
&db,
|
||||||
0,
|
0,
|
||||||
@@ -122,11 +146,18 @@ fn test_carrier_protection_in_storage_and_sync() {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assert!(pull_single_res.is_err(), "sync_single_file_to_host auf Carrier muss fehlschlagen");
|
assert!(
|
||||||
assert!(!local_dest_file.exists(), "Trägerdatei darf niemals auf den Host geschrieben werden");
|
pull_single_res.is_err(),
|
||||||
|
"sync_single_file_to_host auf Carrier muss fehlschlagen"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!local_dest_file.exists(),
|
||||||
|
"Trägerdatei darf niemals auf den Host geschrieben werden"
|
||||||
|
);
|
||||||
|
|
||||||
// 6. R-02: run_sync mit Pull muss die Trägerdatei überspringen
|
// 6. R-02: run_sync mit Pull muss die Trägerdatei überspringen
|
||||||
let local_pull_dir = std::env::temp_dir().join(format!("sanctum_pull_test_{}", std::process::id()));
|
let local_pull_dir =
|
||||||
|
std::env::temp_dir().join(format!("sanctum_pull_test_{}", std::process::id()));
|
||||||
let sync_options = sanctum::sync::SyncOptions {
|
let sync_options = sanctum::sync::SyncOptions {
|
||||||
direction: sanctum::sync::SyncDirection::Pull,
|
direction: sanctum::sync::SyncDirection::Pull,
|
||||||
delete: false,
|
delete: false,
|
||||||
@@ -144,9 +175,15 @@ fn test_carrier_protection_in_storage_and_sync() {
|
|||||||
&local_pull_dir.to_string_lossy(),
|
&local_pull_dir.to_string_lossy(),
|
||||||
&sync_options,
|
&sync_options,
|
||||||
);
|
);
|
||||||
assert!(pull_res.is_ok(), "run_sync pull muss erfolgreich durchlaufen");
|
assert!(
|
||||||
|
pull_res.is_ok(),
|
||||||
|
"run_sync pull muss erfolgreich durchlaufen"
|
||||||
|
);
|
||||||
let pulled_carrier = local_pull_dir.join(carrier_name);
|
let pulled_carrier = local_pull_dir.join(carrier_name);
|
||||||
assert!(!pulled_carrier.exists(), "Trägerdatei darf bei recursive pull nicht auf den Host kopiert werden");
|
assert!(
|
||||||
|
!pulled_carrier.exists(),
|
||||||
|
"Trägerdatei darf bei recursive pull nicht auf den Host kopiert werden"
|
||||||
|
);
|
||||||
let _ = std::fs::remove_dir_all(&local_pull_dir);
|
let _ = std::fs::remove_dir_all(&local_pull_dir);
|
||||||
|
|
||||||
// 7. R-02: VFS Copy Schutz
|
// 7. R-02: VFS Copy Schutz
|
||||||
@@ -166,14 +203,22 @@ fn test_carrier_protection_in_storage_and_sync() {
|
|||||||
let to_copy = DavPath::new("/carrier_copy.iso").unwrap();
|
let to_copy = DavPath::new("/carrier_copy.iso").unwrap();
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
let copy_src_res = rt.block_on(fs.copy(&from_carrier, &to_copy));
|
let copy_src_res = rt.block_on(fs.copy(&from_carrier, &to_copy));
|
||||||
assert!(copy_src_res.is_err(), "SanctumFs::copy mit Carrier als Quelle muss FsError::Forbidden liefern");
|
assert!(
|
||||||
|
copy_src_res.is_err(),
|
||||||
|
"SanctumFs::copy mit Carrier als Quelle muss FsError::Forbidden liefern"
|
||||||
|
);
|
||||||
|
|
||||||
// Copy Ziel = Carrier
|
// Copy Ziel = Carrier
|
||||||
let _regular_file = db.create_node_in_vault(0, 1, "regular.txt", false, &dek_0).unwrap();
|
let _regular_file = db
|
||||||
|
.create_node_in_vault(0, 1, "regular.txt", false, &dek_0)
|
||||||
|
.unwrap();
|
||||||
let from_regular = DavPath::new("/regular.txt").unwrap();
|
let from_regular = DavPath::new("/regular.txt").unwrap();
|
||||||
let to_carrier = DavPath::new(&format!("/{}", carrier_name)).unwrap();
|
let to_carrier = DavPath::new(&format!("/{}", carrier_name)).unwrap();
|
||||||
let copy_dst_res = rt.block_on(fs.copy(&from_regular, &to_carrier));
|
let copy_dst_res = rt.block_on(fs.copy(&from_regular, &to_carrier));
|
||||||
assert!(copy_dst_res.is_err(), "SanctumFs::copy mit Carrier als Ziel muss FsError::Forbidden liefern");
|
assert!(
|
||||||
|
copy_dst_res.is_err(),
|
||||||
|
"SanctumFs::copy mit Carrier als Ziel muss FsError::Forbidden liefern"
|
||||||
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-18
@@ -22,13 +22,15 @@ fn create_test_container(path: &Path) -> (Database, [u8; 32]) {
|
|||||||
let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap();
|
let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap();
|
||||||
let dek = generate_dek();
|
let dek = generate_dek();
|
||||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
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.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||||
|
.unwrap();
|
||||||
(db, *dek)
|
(db, *dek)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_push_and_pull_basic() {
|
fn test_sync_push_and_pull_basic() {
|
||||||
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::<u64>()));
|
let temp_root =
|
||||||
|
std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::<u64>()));
|
||||||
let container_path = temp_root.join("test.sanctum");
|
let container_path = temp_root.join("test.sanctum");
|
||||||
let source_dir = temp_root.join("source");
|
let source_dir = temp_root.join("source");
|
||||||
let target_dir = temp_root.join("restored");
|
let target_dir = temp_root.join("restored");
|
||||||
@@ -38,7 +40,11 @@ fn test_sync_push_and_pull_basic() {
|
|||||||
|
|
||||||
// Testdateien anlegen
|
// Testdateien anlegen
|
||||||
fs::write(source_dir.join("file1.txt"), b"Hello Sanctum Sync!").unwrap();
|
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();
|
fs::write(
|
||||||
|
source_dir.join("sub").join("file2.bin"),
|
||||||
|
vec![0x42u8; 100_000],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let (db, dek) = create_test_container(&container_path);
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
@@ -55,7 +61,8 @@ fn test_sync_push_and_pull_basic() {
|
|||||||
source_dir.to_str().unwrap(),
|
source_dir.to_str().unwrap(),
|
||||||
"/Backup",
|
"/Backup",
|
||||||
&opts,
|
&opts,
|
||||||
).expect("Sync push");
|
)
|
||||||
|
.expect("Sync push");
|
||||||
|
|
||||||
assert_eq!(stats.files_scanned, 2);
|
assert_eq!(stats.files_scanned, 2);
|
||||||
assert_eq!(stats.files_transferred, 2);
|
assert_eq!(stats.files_transferred, 2);
|
||||||
@@ -70,7 +77,8 @@ fn test_sync_push_and_pull_basic() {
|
|||||||
source_dir.to_str().unwrap(),
|
source_dir.to_str().unwrap(),
|
||||||
"/Backup",
|
"/Backup",
|
||||||
&opts,
|
&opts,
|
||||||
).expect("Sync push delta");
|
)
|
||||||
|
.expect("Sync push delta");
|
||||||
|
|
||||||
assert_eq!(stats_delta.files_transferred, 0);
|
assert_eq!(stats_delta.files_transferred, 0);
|
||||||
assert_eq!(stats_delta.files_skipped, 2);
|
assert_eq!(stats_delta.files_skipped, 2);
|
||||||
@@ -88,13 +96,16 @@ fn test_sync_push_and_pull_basic() {
|
|||||||
source_dir.to_str().unwrap(),
|
source_dir.to_str().unwrap(),
|
||||||
"/Backup",
|
"/Backup",
|
||||||
&dry_opts,
|
&dry_opts,
|
||||||
).expect("Sync push dry-run");
|
)
|
||||||
|
.expect("Sync push dry-run");
|
||||||
|
|
||||||
assert_eq!(stats_dry.files_transferred, 1);
|
assert_eq!(stats_dry.files_transferred, 1);
|
||||||
assert_eq!(stats_dry.files_skipped, 2);
|
assert_eq!(stats_dry.files_skipped, 2);
|
||||||
|
|
||||||
// Verifizieren, dass new_file.txt im Tresor tatsächlich NICHT existiert
|
// 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();
|
let node = db
|
||||||
|
.resolve_path_in_vault("/Backup/new_file.txt", 0, &dek)
|
||||||
|
.unwrap();
|
||||||
assert!(node.is_none());
|
assert!(node.is_none());
|
||||||
|
|
||||||
// 4. Pull
|
// 4. Pull
|
||||||
@@ -110,7 +121,8 @@ fn test_sync_push_and_pull_basic() {
|
|||||||
"/Backup",
|
"/Backup",
|
||||||
target_dir.to_str().unwrap(),
|
target_dir.to_str().unwrap(),
|
||||||
&pull_opts,
|
&pull_opts,
|
||||||
).expect("Sync pull");
|
)
|
||||||
|
.expect("Sync pull");
|
||||||
|
|
||||||
assert_eq!(stats_pull.files_transferred, 2);
|
assert_eq!(stats_pull.files_transferred, 2);
|
||||||
|
|
||||||
@@ -126,7 +138,8 @@ fn test_sync_push_and_pull_basic() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_multi_megabyte_large_file() {
|
fn test_sync_multi_megabyte_large_file() {
|
||||||
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_large_{}", rand::random::<u64>()));
|
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 container_path = temp_root.join("test_large.sanctum");
|
||||||
let source_dir = temp_root.join("source");
|
let source_dir = temp_root.join("source");
|
||||||
let target_dir = temp_root.join("restored");
|
let target_dir = temp_root.join("restored");
|
||||||
@@ -159,10 +172,14 @@ fn test_sync_multi_megabyte_large_file() {
|
|||||||
source_dir.to_str().unwrap(),
|
source_dir.to_str().unwrap(),
|
||||||
"/LargeTest",
|
"/LargeTest",
|
||||||
&opts,
|
&opts,
|
||||||
).expect("Sync push large");
|
)
|
||||||
|
.expect("Sync push large");
|
||||||
|
|
||||||
assert_eq!(stats.files_transferred, 1);
|
assert_eq!(stats.files_transferred, 1);
|
||||||
assert_eq!(stats.bytes_transferred, 3 * (CHUNK_SIZE as u64) + 512 * 1024);
|
assert_eq!(
|
||||||
|
stats.bytes_transferred,
|
||||||
|
3 * (CHUNK_SIZE as u64) + 512 * 1024
|
||||||
|
);
|
||||||
|
|
||||||
// Pull
|
// Pull
|
||||||
let mut pull_opts = SyncOptions::default();
|
let mut pull_opts = SyncOptions::default();
|
||||||
@@ -177,7 +194,8 @@ fn test_sync_multi_megabyte_large_file() {
|
|||||||
"/LargeTest",
|
"/LargeTest",
|
||||||
target_dir.to_str().unwrap(),
|
target_dir.to_str().unwrap(),
|
||||||
&pull_opts,
|
&pull_opts,
|
||||||
).expect("Sync pull large");
|
)
|
||||||
|
.expect("Sync pull large");
|
||||||
|
|
||||||
let restored = fs::read(target_dir.join("large_payload.bin")).unwrap();
|
let restored = fs::read(target_dir.join("large_payload.bin")).unwrap();
|
||||||
let original = fs::read(&large_file_path).unwrap();
|
let original = fs::read(&large_file_path).unwrap();
|
||||||
@@ -189,7 +207,8 @@ fn test_sync_multi_megabyte_large_file() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_delete_and_exclude_flags() {
|
fn test_sync_delete_and_exclude_flags() {
|
||||||
let temp_root = std::env::temp_dir().join(format!("sanctum_sync_flags_{}", rand::random::<u64>()));
|
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 container_path = temp_root.join("test_flags.sanctum");
|
||||||
let source_dir = temp_root.join("source");
|
let source_dir = temp_root.join("source");
|
||||||
|
|
||||||
@@ -214,10 +233,14 @@ fn test_sync_delete_and_exclude_flags() {
|
|||||||
source_dir.to_str().unwrap(),
|
source_dir.to_str().unwrap(),
|
||||||
"/Files",
|
"/Files",
|
||||||
&opts,
|
&opts,
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(stats.files_transferred, 2); // keep.txt und remove_me.txt
|
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());
|
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
|
// 2. Lokale Datei löschen und Sync mit --delete ausführen
|
||||||
fs::remove_file(source_dir.join("remove_me.txt")).unwrap();
|
fs::remove_file(source_dir.join("remove_me.txt")).unwrap();
|
||||||
@@ -231,11 +254,18 @@ fn test_sync_delete_and_exclude_flags() {
|
|||||||
source_dir.to_str().unwrap(),
|
source_dir.to_str().unwrap(),
|
||||||
"/Files",
|
"/Files",
|
||||||
&opts,
|
&opts,
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(stats_del.files_deleted, 1);
|
assert_eq!(stats_del.files_deleted, 1);
|
||||||
assert!(db.resolve_path_in_vault("/Files/remove_me.txt", 0, &dek).unwrap().is_none());
|
assert!(db
|
||||||
assert!(db.resolve_path_in_vault("/Files/keep.txt", 0, &dek).unwrap().is_some());
|
.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);
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use sanctum::crypto::{
|
use sanctum::crypto::{
|
||||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
||||||
MIN_TIME_COST,
|
MIN_TIME_COST,
|
||||||
};
|
};
|
||||||
use sanctum::storage::Database;
|
use sanctum::storage::Database;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_reject_arbitrary_sqlite_database() {
|
fn test_reject_arbitrary_sqlite_database() {
|
||||||
@@ -16,12 +16,10 @@ fn test_reject_arbitrary_sqlite_database() {
|
|||||||
// Erstelle eine fremde SQLite-Datenbank mit beliebigen Daten
|
// Erstelle eine fremde SQLite-Datenbank mit beliebigen Daten
|
||||||
{
|
{
|
||||||
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
conn.execute(
|
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", [])
|
||||||
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
|
.unwrap();
|
||||||
[],
|
conn.execute("INSERT INTO users (name) VALUES ('Alice')", [])
|
||||||
)
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
conn.execute("INSERT INTO users (name) VALUES ('Alice')", []).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanctum Database::open muss die Datei sofort fail-closed ablehnen
|
// Sanctum Database::open muss die Datei sofort fail-closed ablehnen
|
||||||
@@ -114,7 +112,8 @@ fn test_reject_out_of_bounds_kdf_params() {
|
|||||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&db_path).unwrap();
|
let db = Database::open(&db_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
drop(db);
|
drop(db);
|
||||||
|
|
||||||
@@ -169,7 +168,8 @@ fn test_reject_out_of_bounds_kdf_params() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_reject_corrupted_slot_lengths() {
|
fn test_reject_corrupted_slot_lengths() {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let db_path: PathBuf = temp_dir.join(format!("test_slot_lengths_{}.sanctum", std::process::id()));
|
let db_path: PathBuf =
|
||||||
|
temp_dir.join(format!("test_slot_lengths_{}.sanctum", std::process::id()));
|
||||||
if db_path.exists() {
|
if db_path.exists() {
|
||||||
let _ = std::fs::remove_file(&db_path);
|
let _ = std::fs::remove_file(&db_path);
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,8 @@ fn test_reject_corrupted_slot_lengths() {
|
|||||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
let db = Database::open(&db_path).unwrap();
|
let db = Database::open(&db_path).unwrap();
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag).unwrap();
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
db.checkpoint().unwrap();
|
db.checkpoint().unwrap();
|
||||||
drop(db);
|
drop(db);
|
||||||
|
|
||||||
@@ -214,3 +215,465 @@ fn test_reject_corrupted_slot_lengths() {
|
|||||||
|
|
||||||
let _ = std::fs::remove_file(&db_path);
|
let _ = std::fs::remove_file(&db_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_slot_amplification_and_excessive_slots_dos() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("TestPassword2026!", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
let params_json = serde_json::to_string(&kdf_params).unwrap();
|
||||||
|
|
||||||
|
// 1. Fall: 0 Slots (leere meta-Tabelle)
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_0_slots_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER PRIMARY KEY, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute("CREATE TABLE nodes (id INTEGER PRIMARY KEY);", [])
|
||||||
|
.unwrap();
|
||||||
|
conn.execute("CREATE TABLE chunks (node_id INTEGER);", [])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
let res = db.read_slots();
|
||||||
|
assert!(res.is_err());
|
||||||
|
let err_msg = res.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("leer oder beschädigt"),
|
||||||
|
"Fehlermeldung: {err_msg}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall: Genau 1 Slot (Slot 0 Decoy/Standard) -> MUSS erfolgreich sein
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_1_slot_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
// Lösche Dummy-Slot 1, um reinen 1-Slot Container zu simulieren
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute("DELETE FROM meta WHERE slot_id = 1", [])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let db_reopened = Database::open(&db_path).unwrap();
|
||||||
|
let slots = db_reopened
|
||||||
|
.read_slots()
|
||||||
|
.expect("1 gültiger Slot (Slot 0) muss erfolgreich gelesen werden");
|
||||||
|
assert_eq!(slots.len(), 1);
|
||||||
|
assert_eq!(slots[0].slot_id, 0);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fall: Genau 2 Slots (Slot 0 & Slot 1) -> MUSS erfolgreich sein
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_2_slots_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||||
|
.unwrap();
|
||||||
|
let slots = db
|
||||||
|
.read_slots()
|
||||||
|
.expect("2 gültige Slots müssen erfolgreich gelesen werden");
|
||||||
|
assert_eq!(slots.len(), 2);
|
||||||
|
assert_eq!(slots[0].slot_id, 0);
|
||||||
|
assert_eq!(slots[1].slot_id, 1);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fall: 3 Slots (Überschreitung des Limits von 2) -> MUSS fail-closed abgewehrt werden
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_3_slots_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for i in 0..3 {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO meta VALUES (?1, ?2, 2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
rusqlite::params![
|
||||||
|
i,
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let open_res = Database::open(&db_path);
|
||||||
|
assert!(
|
||||||
|
open_res.is_err(),
|
||||||
|
"Database::open muss 3 Slots sofort abweisen"
|
||||||
|
);
|
||||||
|
let err_msg = open_res.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Ungültige Slot-Anzahl"),
|
||||||
|
"Fehlermeldung: {err_msg}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fall: 100 Slots (KDF-Amplification DoS Angriff) -> MUSS sofort ohne KDF abgewehrt werden
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_100_slots_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for i in 0..100 {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO meta VALUES (?1, ?2, 2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
rusqlite::params![
|
||||||
|
i,
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let open_res = Database::open(&db_path);
|
||||||
|
assert!(
|
||||||
|
open_res.is_err(),
|
||||||
|
"100 Slots müssen sofort abgewiesen werden"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Fall: 1000 Slots (Massiver DoS-Angriff) -> MUSS sofort abgewehrt werden
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_1000_slots_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("INSERT INTO meta VALUES (?1, ?2, 2, ?3, ?4, ?5, ?6, ?7)")
|
||||||
|
.unwrap();
|
||||||
|
for i in 0..1000 {
|
||||||
|
stmt.execute(rusqlite::params![
|
||||||
|
i,
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let open_res = Database::open(&db_path);
|
||||||
|
assert!(
|
||||||
|
open_res.is_err(),
|
||||||
|
"1000 Slots müssen sofort abgewiesen werden"
|
||||||
|
);
|
||||||
|
let err_msg = open_res.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Ungültige Slot-Anzahl"),
|
||||||
|
"Fehlermeldung: {err_msg}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_duplicate_and_invalid_slot_ids() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("TestPassword2026!", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
let params_json = serde_json::to_string(&kdf_params).unwrap();
|
||||||
|
|
||||||
|
// 1. Fall: Doppelte Slot-ID (zwei Slots mit slot_id = 0)
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!("test_dup_slot_0_{}.sanctum", std::process::id()));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO meta VALUES (0, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
rusqlite::params![
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO meta VALUES (0, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
rusqlite::params![
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
let res = db.read_slots();
|
||||||
|
assert!(res.is_err(), "Doppelte Slot-ID 0 muss abgewiesen werden");
|
||||||
|
let err_msg = res.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Doppelte Slot-ID 0"),
|
||||||
|
"Fehlermeldung: {err_msg}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall: Ungültige Slot-ID (z. B. slot_id = 5 statt 0 oder 1)
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!(
|
||||||
|
"test_invalid_slot_id_{}.sanctum",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO meta VALUES (5, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
rusqlite::params![
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
let res = db.read_slots();
|
||||||
|
assert!(res.is_err(), "Ungültige Slot-ID 5 muss abgewiesen werden");
|
||||||
|
let err_msg = res.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Ungültige Slot-ID 5"),
|
||||||
|
"Fehlermeldung: {err_msg}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fall: Fehlender Slot 0 (nur Slot 1 vorhanden)
|
||||||
|
{
|
||||||
|
let db_path = temp_dir.join(format!(
|
||||||
|
"test_missing_slot_0_{}.sanctum",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE meta (
|
||||||
|
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
|
||||||
|
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
|
||||||
|
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
|
||||||
|
);",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO meta VALUES (1, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
rusqlite::params![
|
||||||
|
b"SANCTUM\0",
|
||||||
|
&salt[..],
|
||||||
|
¶ms_json,
|
||||||
|
&wrapped_dek[..],
|
||||||
|
&nonce[..],
|
||||||
|
&tag[..]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let db = Database::open(&db_path).unwrap();
|
||||||
|
let res = db.read_slots();
|
||||||
|
assert!(res.is_err(), "Fehlender Slot 0 muss abgewiesen werden");
|
||||||
|
let err_msg = res.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Slot 0 (Standard/Decoy Vault) fehlt"),
|
||||||
|
"Fehlermeldung: {err_msg}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(&db_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_container_meta_authenticate_defense_in_depth() {
|
||||||
|
use sanctum::storage::{ContainerMeta, SlotMeta};
|
||||||
|
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
let kek = derive_kek("TestPassword2026!", &salt, &kdf_params).unwrap();
|
||||||
|
let dek = generate_dek();
|
||||||
|
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||||
|
|
||||||
|
let valid_slot_0 = SlotMeta {
|
||||||
|
slot_id: 0,
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid_slot_1 = SlotMeta {
|
||||||
|
slot_id: 1,
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A. Leere Slots -> sofort None
|
||||||
|
let meta_empty = ContainerMeta {
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
slots: vec![],
|
||||||
|
};
|
||||||
|
assert!(meta_empty.authenticate("TestPassword2026!").is_none());
|
||||||
|
|
||||||
|
// B. Mehr als 2 Slots -> sofort None (keine KDF-Berechnung!)
|
||||||
|
let meta_3_slots = ContainerMeta {
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
slots: vec![
|
||||||
|
valid_slot_0.clone(),
|
||||||
|
valid_slot_1.clone(),
|
||||||
|
valid_slot_0.clone(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert!(meta_3_slots.authenticate("TestPassword2026!").is_none());
|
||||||
|
|
||||||
|
// C. Fehlender Slot 0 (nur Slot 1) -> sofort None
|
||||||
|
let meta_no_slot0 = ContainerMeta {
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
slots: vec![valid_slot_1.clone()],
|
||||||
|
};
|
||||||
|
assert!(meta_no_slot0.authenticate("TestPassword2026!").is_none());
|
||||||
|
|
||||||
|
// D. Ungültige Slot-ID (> 1) -> sofort None
|
||||||
|
let mut invalid_slot = valid_slot_0.clone();
|
||||||
|
invalid_slot.slot_id = 99;
|
||||||
|
let meta_invalid_id = ContainerMeta {
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
slots: vec![valid_slot_0.clone(), invalid_slot],
|
||||||
|
};
|
||||||
|
assert!(meta_invalid_id.authenticate("TestPassword2026!").is_none());
|
||||||
|
|
||||||
|
// E. Korrekter 1-Slot Container -> Erfolgreiche Authentifizierung
|
||||||
|
let meta_valid_1 = ContainerMeta {
|
||||||
|
version: 2,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce: nonce,
|
||||||
|
header_tag: tag,
|
||||||
|
slots: vec![valid_slot_0.clone()],
|
||||||
|
};
|
||||||
|
assert!(meta_valid_1.authenticate("TestPassword2026!").is_some());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use sanctum::upgrade::{
|
use sanctum::upgrade::{
|
||||||
run_upgrade, verify_minisign_signature, UpgradeOptions, SANCTUM_RELEASE_PUBKEY,
|
run_upgrade, verify_minisign_signature, UpgradeOptions, SANCTUM_RELEASE_PUBKEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12,7 +12,11 @@ trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\
|
|||||||
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
||||||
|
|
||||||
let res = verify_minisign_signature(payload, sig_text, SANCTUM_RELEASE_PUBKEY);
|
let res = verify_minisign_signature(payload, sig_text, SANCTUM_RELEASE_PUBKEY);
|
||||||
assert!(res.is_ok(), "Gültige Minisign-Signatur muss erfolgreich verifiziert werden: {:?}", res.err());
|
assert!(
|
||||||
|
res.is_ok(),
|
||||||
|
"Gültige Minisign-Signatur muss erfolgreich verifiziert werden: {:?}",
|
||||||
|
res.err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -25,7 +29,10 @@ trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\
|
|||||||
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
0l7KsNJkDTo2uOKL8UAiaJOWuhwhGTVZ5fn0JFMdxXE9riYixQO9YnBpSVqz8iCDhWrz8hJBnmUrIWnaRaXVCA==\n";
|
||||||
|
|
||||||
let res = verify_minisign_signature(tampered_payload, sig_text, SANCTUM_RELEASE_PUBKEY);
|
let res = verify_minisign_signature(tampered_payload, sig_text, SANCTUM_RELEASE_PUBKEY);
|
||||||
assert!(res.is_err(), "Manipulierte Binärdaten müssen die Minisign-Prüfung verfehlen");
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"Manipulierte Binärdaten müssen die Minisign-Prüfung verfehlen"
|
||||||
|
);
|
||||||
assert!(res.unwrap_err().to_string().contains("FEHLGESCHLAGEN"));
|
assert!(res.unwrap_err().to_string().contains("FEHLGESCHLAGEN"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +48,10 @@ trusted comment: timestamp:1789750897\tfile:test_payload.txt\thashed\n\
|
|||||||
// Ein anderer, unautorisierter Public Key
|
// Ein anderer, unautorisierter Public Key
|
||||||
let other_key = "RWSb0a70vF1X8qIjc7xi28Gmw+cIWbMipOy4L6ToJEUPkWDw3c0qIjc7";
|
let other_key = "RWSb0a70vF1X8qIjc7xi28Gmw+cIWbMipOy4L6ToJEUPkWDw3c0qIjc7";
|
||||||
let res = verify_minisign_signature(payload, sig_text, other_key);
|
let res = verify_minisign_signature(payload, sig_text, other_key);
|
||||||
assert!(res.is_err(), "Signaturprüfung mit fremdem Schlüssel muss fehlschlagen");
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"Signaturprüfung mit fremdem Schlüssel muss fehlschlagen"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -55,7 +65,10 @@ fn test_upgrade_blocks_unofficial_url_without_insecure_flag() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let res = run_upgrade(&options);
|
let res = run_upgrade(&options);
|
||||||
assert!(res.is_err(), "Inoffizielle Server-URL ohne --insecure-url muss blockiert werden");
|
assert!(
|
||||||
|
res.is_err(),
|
||||||
|
"Inoffizielle Server-URL ohne --insecure-url muss blockiert werden"
|
||||||
|
);
|
||||||
let err_msg = res.unwrap_err().to_string();
|
let err_msg = res.unwrap_err().to_string();
|
||||||
assert!(
|
assert!(
|
||||||
err_msg.contains("Sicherheitsverstoß") || err_msg.contains("nicht vertrauenswürdig"),
|
err_msg.contains("Sicherheitsverstoß") || err_msg.contains("nicht vertrauenswürdig"),
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ fn test_json_deserialization_of_gitea_release() {
|
|||||||
]
|
]
|
||||||
}"##;
|
}"##;
|
||||||
|
|
||||||
let release: GiteaRelease = serde_json::from_str(gitea_json).expect("Deserialisierung erfolgreich");
|
let release: GiteaRelease =
|
||||||
|
serde_json::from_str(gitea_json).expect("Deserialisierung erfolgreich");
|
||||||
assert_eq!(release.id, 27);
|
assert_eq!(release.id, 27);
|
||||||
assert_eq!(release.tag_name, "v0.5.0");
|
assert_eq!(release.tag_name, "v0.5.0");
|
||||||
assert_eq!(release.assets.len(), 3);
|
assert_eq!(release.assets.len(), 3);
|
||||||
|
|||||||
Reference in New Issue
Block a user