feat(anti-forensics): implement incremental auto-vacuum, chunk shredding, and plausible deniability
This commit is contained in:
@@ -129,6 +129,70 @@ pub fn unwrap_dek(
|
||||
Ok(dek)
|
||||
}
|
||||
|
||||
/// Erzeugt einen Dummy-Header-Slot mit kryptografisch sicherem Zufallsrauschen derselben Länge wie
|
||||
/// ein echter KDF/DEK-Slot. Dadurch sind Standard-Container von Containern mit Hidden Vault
|
||||
/// auf Bitebene und Entropieebene ununterscheidbar (Plausible Deniability).
|
||||
pub fn generate_dummy_slot() -> (Vec<u8>, [u8; 12], [u8; 16], [u8; 16]) {
|
||||
let mut wrapped_dek = vec![0u8; 32];
|
||||
let mut nonce = [0u8; 12];
|
||||
let mut tag = [0u8; 16];
|
||||
let mut salt = [0u8; 16];
|
||||
OsRng.fill_bytes(&mut wrapped_dek);
|
||||
OsRng.fill_bytes(&mut nonce);
|
||||
OsRng.fill_bytes(&mut tag);
|
||||
OsRng.fill_bytes(&mut salt);
|
||||
(wrapped_dek, nonce, tag, salt)
|
||||
}
|
||||
|
||||
/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM.
|
||||
/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind.
|
||||
pub fn encrypt_node_name(dek: &[u8; 32], name: &str) -> String {
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let cipher = Aes256Gcm::new_from_slice(dek).expect("AES init");
|
||||
let mut buffer = name.as_bytes().to_vec();
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), b"SANCTUM_NODE_NAME", &mut buffer)
|
||||
.expect("Name encryption");
|
||||
format!(
|
||||
"$h${}${}${}",
|
||||
hex::encode(nonce_bytes),
|
||||
hex::encode(tag.as_slice()),
|
||||
hex::encode(&buffer)
|
||||
)
|
||||
}
|
||||
|
||||
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
|
||||
pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option<String> {
|
||||
if let Some(rest) = stored.strip_prefix("$h$") {
|
||||
let parts: Vec<&str> = rest.split('$').collect();
|
||||
if parts.len() == 3 {
|
||||
if let (Ok(nonce_bytes), Ok(tag_bytes), Ok(ct_bytes)) = (
|
||||
hex::decode(parts[0]),
|
||||
hex::decode(parts[1]),
|
||||
hex::decode(parts[2]),
|
||||
) {
|
||||
if nonce_bytes.len() == 12 && tag_bytes.len() == 16 {
|
||||
let cipher = Aes256Gcm::new_from_slice(dek).ok()?;
|
||||
let mut buffer = ct_bytes;
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(&nonce_bytes),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer,
|
||||
Tag::from_slice(&tag_bytes),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Kodiert den 32-Byte (256-Bit) DEK in eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase (englisch) mit 8-Bit Checksumme.
|
||||
pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
|
||||
let mnemonic = bip39::Mnemonic::from_entropy(dek)
|
||||
@@ -412,5 +476,28 @@ mod tests {
|
||||
let corrupted_phrase = words.join(" ");
|
||||
assert!(mnemonic_to_dek(&corrupted_phrase).is_err(), "Checksum check must fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hidden_node_name_encryption_and_dummy_slot() {
|
||||
let dek = generate_dek();
|
||||
let filename = "ultra_geheimes_dokument.pdf";
|
||||
let encrypted = encrypt_node_name(&dek, filename);
|
||||
assert!(encrypted.starts_with("$h$"));
|
||||
assert!(!encrypted.contains(filename));
|
||||
|
||||
let decrypted = decrypt_node_name(&dek, &encrypted).expect("Decrypt name");
|
||||
assert_eq!(decrypted, filename);
|
||||
|
||||
// Mit anderem DEK schlägt Entschlüsselung fehl
|
||||
let other_dek = generate_dek();
|
||||
assert!(decrypt_node_name(&other_dek, &encrypted).is_none());
|
||||
|
||||
// Dummy-Slot hat korrekte Längen
|
||||
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
|
||||
assert_eq!(dummy_dek.len(), 32);
|
||||
assert_eq!(dummy_nonce.len(), 12);
|
||||
assert_eq!(dummy_tag.len(), 16);
|
||||
assert_eq!(dummy_salt.len(), 16);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+250
-60
@@ -33,6 +33,21 @@ enum Commands {
|
||||
/// Pfad zur zu erstellenden .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Erstellt zusätzlich einen plausibel abstreitbaren Hidden Vault (Plausible Deniability)
|
||||
#[arg(long, default_value_t = false)]
|
||||
with_hidden: bool,
|
||||
},
|
||||
|
||||
/// Kompaktiert den Container-Speicherplatz (Incremental Vacuum) und bereinigt ungenutzte Seiten
|
||||
Compact {
|
||||
/// Pfad zur .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Maximale Anzahl an Seiten, die freigegeben werden sollen (Standard: alle freien Seiten)
|
||||
#[arg(short, long)]
|
||||
pages: Option<usize>,
|
||||
},
|
||||
|
||||
/// Bindet einen Sanctum-Container als Windows-Netzlaufwerk via WebDAV ein
|
||||
@@ -157,7 +172,7 @@ fn parse_drive_letter(s: &str) -> Result<char> {
|
||||
Ok(ch.to_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn handle_init(container_path: &Path) -> Result<()> {
|
||||
fn handle_init(container_path: &Path, with_hidden: bool) -> Result<()> {
|
||||
if container_path.exists() {
|
||||
bail!(
|
||||
"Zieldatei '{}' existiert bereits. Initialisierung abgebrochen, um Überschreiben zu verhindern.",
|
||||
@@ -169,65 +184,208 @@ fn handle_init(container_path: &Path) -> Result<()> {
|
||||
println!("│ Sanctum — Neuen verschlüsselten Container erstellen │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Zieldatei: {}", container_path.display());
|
||||
if with_hidden {
|
||||
println!(" Modus: Dual-Vault (Standard + {})", ui::magenta("Hidden Vault"));
|
||||
}
|
||||
println!();
|
||||
|
||||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
if with_hidden {
|
||||
println!(" ─── [1/2] Standard-Vault (Äußerer Container / Decoy) ───");
|
||||
let password_0 = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
if password_0.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
let confirm_0 = rpassword::prompt_password("Master-Passwort für Standard-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
if password_0 != confirm_0 {
|
||||
bail!("Die eingegebenen Passwörter für den Standard-Vault stimmen nicht überein!");
|
||||
}
|
||||
|
||||
if password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
println!();
|
||||
println!(" ─── [2/2] Hidden Vault (Versteckter Speicher / Plausible Deniability) ───");
|
||||
println!(" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG"));
|
||||
let password_1 = rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
if password_1.trim().is_empty() {
|
||||
bail!("Das Master-Passwort für den Hidden-Vault darf nicht leer sein.");
|
||||
}
|
||||
if password_1 == password_0 {
|
||||
bail!("Das Hidden-Vault-Passwort darf nicht mit dem Standard-Passwort identisch sein!");
|
||||
}
|
||||
let confirm_1 = rpassword::prompt_password("Master-Passwort für Hidden-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
if password_1 != confirm_1 {
|
||||
bail!("Die eingegebenen Passwörter für den Hidden-Vault stimmen nicht überein!");
|
||||
}
|
||||
|
||||
println!();
|
||||
ui::step(1, 4, "🔑", "Leite KEKs für Standard- und Hidden-Vault via Argon2id ab...");
|
||||
let salt_0 = generate_salt();
|
||||
let kdf_params_0 = KdfParams::default();
|
||||
let kek_0 = derive_kek(&password_0, &salt_0, &kdf_params_0)?;
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kdf_params_1 = KdfParams::default();
|
||||
let kek_1 = derive_kek(&password_1, &salt_1, &kdf_params_1)?;
|
||||
|
||||
ui::step(2, 4, "🎲", "Erzeuge getrennte DEKs für beide Vaults via CSPRNG...");
|
||||
let dek_0 = generate_dek();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
ui::step(3, 4, "🔒", "Verschlüssele beide DEKs unabhängig via AES-256-GCM...");
|
||||
let (wrapped_dek_0, nonce_0, tag_0) = wrap_dek(&kek_0, &dek_0)?;
|
||||
let (wrapped_dek_1, nonce_1, tag_1) = wrap_dek(&kek_1, &dek_1)?;
|
||||
|
||||
ui::step(4, 4, "📦", "Initialisiere SQLite-Container mit Dual-Slot Header & WAL-Modus...");
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||
|
||||
db.init_schema_with_hidden(
|
||||
&salt_0,
|
||||
&kdf_params_0,
|
||||
&wrapped_dek_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((&salt_1, &kdf_params_1, &wrapped_dek_1, &nonce_1, &tag_1)),
|
||||
)?;
|
||||
|
||||
db.checkpoint()?;
|
||||
|
||||
let phrase_0 = dek_to_mnemonic(&dek_0)?;
|
||||
let phrase_1 = dek_to_mnemonic(&dek_1)?;
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum Dual-Vault Container erfolgreich initialisiert! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
||||
println!(" • Verschlüssel: AES-256-GCM + LZ4-Kompression + Dateinamens-Verschleierung");
|
||||
println!(" • Deniability: Beide Slots besitzen identische Struktur und Bit-Entropie");
|
||||
println!();
|
||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||||
println!(" (Die Eingabe des jeweiligen Passworts bindet automatisch den passenden Vault ein)");
|
||||
println!();
|
||||
|
||||
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||||
println!(" │ NOTFALLSCHLÜSSEL — SLOT 0 (STANDARD / DECOY VAULT) │");
|
||||
println!(" └─────────────────────────────────────────────────────────┘");
|
||||
ui::print_recovery_phrase_card(&phrase_0);
|
||||
|
||||
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||||
println!(" │ NOTFALLSCHLÜSSEL — SLOT 1 (HIDDEN VAULT) │");
|
||||
println!(" └─────────────────────────────────────────────────────────┘");
|
||||
ui::print_recovery_phrase_card(&phrase_1);
|
||||
} else {
|
||||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
|
||||
if password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
|
||||
let confirm_password = rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
|
||||
if password != confirm_password {
|
||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||
}
|
||||
|
||||
println!();
|
||||
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams::default();
|
||||
let kek = derive_kek(&password, &salt, &kdf_params)
|
||||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||||
|
||||
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
||||
let dek = generate_dek();
|
||||
|
||||
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
||||
let (wrapped_dek, header_nonce, header_tag) =
|
||||
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
||||
|
||||
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||
.context("Fehler bei der Schema-Initialisierung")?;
|
||||
|
||||
db.checkpoint()
|
||||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||||
|
||||
let recovery_phrase =
|
||||
dek_to_mnemonic(&dek).context("Fehler beim Erzeugen der Notfallphrase")?;
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
||||
println!(" • Deniability: Slot 1 mit CSPRNG-Zufallsrauschen gefüllt");
|
||||
println!();
|
||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||||
println!();
|
||||
|
||||
ui::print_recovery_phrase_card(&recovery_phrase);
|
||||
}
|
||||
|
||||
let confirm_password = rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if password != confirm_password {
|
||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||
fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Storage-Kompaktierung (Anti-Forensics) │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!();
|
||||
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams::default();
|
||||
let kek = derive_kek(&password, &salt, &kdf_params)
|
||||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||||
|
||||
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
||||
let dek = generate_dek();
|
||||
|
||||
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
||||
let (wrapped_dek, header_nonce, header_tag) =
|
||||
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
||||
|
||||
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||
.context("Fehler bei der Schema-Initialisierung")?;
|
||||
let freelist_before = db.freelist_count()
|
||||
.context("Konnte Freelist-Größe nicht ermitteln")?;
|
||||
|
||||
println!(" • Freie Seiten in Freelist: {}", ui::cyan(&freelist_before.to_string()));
|
||||
|
||||
if freelist_before == 0 {
|
||||
println!(" • Der Container ist bereits optimal kompaktiert. Keine Bereinigung nötig.");
|
||||
println!();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ui::step(1, 2, "🧹", "Führe Incremental-Vacuum aus...");
|
||||
let freed = db.incremental_vacuum(pages)
|
||||
.context("Fehler bei der Speicherbereinigung (Incremental Vacuum)")?;
|
||||
|
||||
ui::step(2, 2, "💾", "Führe finalen WAL-Checkpoint durch...");
|
||||
db.checkpoint()
|
||||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||||
|
||||
let recovery_phrase =
|
||||
dek_to_mnemonic(&dek).context("Fehler beim Erzeugen der Notfallphrase")?;
|
||||
let freelist_after = db.freelist_count().unwrap_or(0);
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │");
|
||||
println!("│ ✔ Container erfolgreich kompaktiert! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
||||
println!(" • Freigegebene Seiten: {}", ui::green(&freed.to_string()));
|
||||
println!(" • Verbleibende Freeseiten: {}", ui::dim(&freelist_after.to_string()));
|
||||
println!();
|
||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\" --drive S", container_path.display())));
|
||||
println!();
|
||||
|
||||
ui::print_recovery_phrase_card(&recovery_phrase);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -248,11 +406,11 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
let dek = if let Some(phrase) = recovery_key {
|
||||
let (dek, target_slot_id) = if let Some(phrase) = recovery_key {
|
||||
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
||||
let d = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||||
d
|
||||
(d, 0)
|
||||
} else {
|
||||
let old_password = rpassword::prompt_password("Aktuelles Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des aktuellen Passworts")?;
|
||||
@@ -268,16 +426,22 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
.context("Konnte Container-Header nicht lesen")?;
|
||||
|
||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab & prüfe Passwort...");
|
||||
let old_kek = derive_kek(&old_password, &meta.kdf_salt, &meta.kdf_params)
|
||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
||||
let mut found = None;
|
||||
for slot in &meta.slots {
|
||||
if let Ok(old_kek) = derive_kek(&old_password, &slot.kdf_salt, &slot.kdf_params) {
|
||||
if let Ok(d) = unwrap_dek(
|
||||
&old_kek,
|
||||
&slot.wrapped_dek,
|
||||
&slot.header_nonce,
|
||||
&slot.header_tag,
|
||||
) {
|
||||
found = Some((d, slot.slot_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unwrap_dek(
|
||||
&old_kek,
|
||||
&meta.wrapped_dek,
|
||||
&meta.header_nonce,
|
||||
&meta.header_tag,
|
||||
)
|
||||
.map_err(|_| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?
|
||||
found.ok_or_else(|| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?
|
||||
};
|
||||
|
||||
println!();
|
||||
@@ -306,7 +470,8 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
wrap_dek(&new_kek, &dek).context("DEK-Wrapping mit neuem KEK fehlgeschlagen")?;
|
||||
|
||||
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
|
||||
db.update_meta_keys(
|
||||
db.update_slot_keys(
|
||||
target_slot_id,
|
||||
&new_salt,
|
||||
&new_params,
|
||||
&new_wrapped_dek,
|
||||
@@ -324,6 +489,7 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Vault Slot: {}", target_slot_id);
|
||||
println!(" • KDF: Argon2id mit frischem Salt");
|
||||
println!(" • Status: DEK sicher neu verpackt (alle Chunks intakt)");
|
||||
println!();
|
||||
@@ -425,12 +591,23 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||
let kek = derive_kek(&password, &meta.kdf_salt, &meta.kdf_params)
|
||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
||||
let dek = unwrap_dek(&kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag)
|
||||
.map_err(|_| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||
|
||||
let mut found = None;
|
||||
for slot in &meta.slots {
|
||||
if let Ok(kek) = derive_kek(&password, &slot.kdf_salt, &slot.kdf_params) {
|
||||
if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
|
||||
found = Some((dek, slot.slot_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (dek, slot_id) = found.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||
|
||||
let phrase = dek_to_mnemonic(&dek)?;
|
||||
if slot_id == 1 {
|
||||
println!(" • Vault: {}", ui::magenta("Hidden Vault (Slot 1)"));
|
||||
}
|
||||
ui::print_recovery_phrase_card(&phrase);
|
||||
|
||||
Ok(())
|
||||
@@ -453,13 +630,23 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||||
let dek = if !password.trim().is_empty() {
|
||||
let db = Database::open(container_path).context("Konnte Container nicht öffnen")?;
|
||||
let meta = db.read_meta().context("Konnte Header nicht lesen")?;
|
||||
let kek = derive_kek(&password, &meta.kdf_salt, &meta.kdf_params)?;
|
||||
match unwrap_dek(&kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag) {
|
||||
Ok(d) => {
|
||||
|
||||
let mut found = None;
|
||||
for slot in &meta.slots {
|
||||
if let Ok(kek) = derive_kek(&password, &slot.kdf_salt, &slot.kdf_params) {
|
||||
if let Ok(d) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
|
||||
found = Some(d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match found {
|
||||
Some(d) => {
|
||||
println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"));
|
||||
Some(d)
|
||||
}
|
||||
Err(_) => {
|
||||
None => {
|
||||
println!(" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.", ui::yellow("⚠️"));
|
||||
None
|
||||
}
|
||||
@@ -483,8 +670,11 @@ async fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Init { path } => {
|
||||
handle_init(&path)?;
|
||||
Commands::Init { path, with_hidden } => {
|
||||
handle_init(&path, with_hidden)?;
|
||||
}
|
||||
Commands::Compact { path, pages } => {
|
||||
handle_compact(&path, pages)?;
|
||||
}
|
||||
Commands::Mount {
|
||||
path,
|
||||
|
||||
+53
-15
@@ -107,33 +107,56 @@ pub async fn mount_container(
|
||||
.read_meta()
|
||||
.context("Konnte Container-Header nicht lesen")?;
|
||||
|
||||
let (dek, version) = match auth {
|
||||
let (dek, version, vault_id) = match auth {
|
||||
ContainerAuth::Password(ref password) => {
|
||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
|
||||
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
||||
let mut unwrapped = None;
|
||||
|
||||
ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM...");
|
||||
let dek = unwrap_dek(
|
||||
&kek,
|
||||
&meta.wrapped_dek,
|
||||
&meta.header_nonce,
|
||||
&meta.header_tag,
|
||||
)
|
||||
.context("Ungültiges Master-Passwort oder Container beschädigt")?;
|
||||
(dek, meta.version)
|
||||
for slot in &meta.slots {
|
||||
if let Ok(kek) = derive_kek(password, &slot.kdf_salt, &slot.kdf_params) {
|
||||
if let Ok(dek) = unwrap_dek(
|
||||
&kek,
|
||||
&slot.wrapped_dek,
|
||||
&slot.header_nonce,
|
||||
&slot.header_tag,
|
||||
) {
|
||||
unwrapped = Some((dek, slot.version, slot.slot_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match unwrapped {
|
||||
Some((dek, ver, slot_id)) => {
|
||||
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
|
||||
(dek, ver, slot_id)
|
||||
}
|
||||
None => {
|
||||
bail!("Ungültiges Master-Passwort oder Container beschädigt");
|
||||
}
|
||||
}
|
||||
}
|
||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
||||
let dek = mnemonic_to_dek(phrase)
|
||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
||||
(dek, meta.version)
|
||||
|
||||
let vault_id = if db.has_hidden_vault().unwrap_or(false) {
|
||||
let is_hidden = {
|
||||
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||
!children.is_empty()
|
||||
};
|
||||
if is_hidden { 1 } else { 0 }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(dek, meta.version, vault_id)
|
||||
}
|
||||
};
|
||||
|
||||
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield)
|
||||
let fs = SanctumFs::with_options(db.clone(), dek, version, anti_leak);
|
||||
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Vault-Routing)
|
||||
let fs = SanctumFs::with_vault(db.clone(), dek, version, anti_leak, vault_id);
|
||||
let last_activity = fs.last_activity();
|
||||
let dav_server = DavHandler::builder()
|
||||
.filesystem(Box::new(fs))
|
||||
@@ -300,6 +323,9 @@ pub async fn mount_container(
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
||||
if vault_id == 1 {
|
||||
println!(" • Vault: {} (Verschlüsselte Dateinamen)", ui::magenta("Hidden Vault (Slot 1)"));
|
||||
}
|
||||
if let Some(secs) = idle_timeout {
|
||||
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
||||
}
|
||||
@@ -350,6 +376,18 @@ pub async fn mount_container(
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
|
||||
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
|
||||
if let Ok(freelist) = db.freelist_count() {
|
||||
if freelist > 0 {
|
||||
print!(" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
match db.incremental_vacuum(None) {
|
||||
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
|
||||
Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite WAL Checkpoint erzwingen
|
||||
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
+23
-1
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::crypto::{
|
||||
derive_kek, mnemonic_to_dek, wrap_dek, KdfParams, FORMAT_VERSION,
|
||||
};
|
||||
use crate::storage::{ContainerMeta, Database};
|
||||
use crate::storage::{ContainerMeta, Database, SlotMeta};
|
||||
|
||||
pub const HEADER_BACKUP_MAGIC: &str = "SANCTUM_HEADER_BACKUP";
|
||||
pub const CURRENT_BACKUP_VERSION: u32 = 1;
|
||||
@@ -79,6 +79,16 @@ impl HeaderBackup {
|
||||
let mut header_tag = [0u8; 16];
|
||||
header_tag.copy_from_slice(&tag_bytes);
|
||||
|
||||
let slot0 = SlotMeta {
|
||||
slot_id: 0,
|
||||
version: self.container_format_version,
|
||||
kdf_salt,
|
||||
kdf_params: self.kdf_params.clone(),
|
||||
wrapped_dek: wrapped_dek.clone(),
|
||||
header_nonce,
|
||||
header_tag,
|
||||
};
|
||||
|
||||
Ok(ContainerMeta {
|
||||
version: self.container_format_version,
|
||||
kdf_salt,
|
||||
@@ -86,6 +96,7 @@ impl HeaderBackup {
|
||||
wrapped_dek,
|
||||
header_nonce,
|
||||
header_tag,
|
||||
slots: vec![slot0],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -166,6 +177,16 @@ pub fn restore_header_from_recovery_key(
|
||||
let (wrapped_dek, header_nonce, header_tag) =
|
||||
wrap_dek(&kek, &dek).context("Verschlüsseln des DEK fehlgeschlagen")?;
|
||||
|
||||
let slot0 = SlotMeta {
|
||||
slot_id: 0,
|
||||
version: FORMAT_VERSION,
|
||||
kdf_salt: salt,
|
||||
kdf_params: kdf_params.clone(),
|
||||
wrapped_dek: wrapped_dek.clone(),
|
||||
header_nonce,
|
||||
header_tag,
|
||||
};
|
||||
|
||||
let meta = ContainerMeta {
|
||||
version: FORMAT_VERSION,
|
||||
kdf_salt: salt,
|
||||
@@ -173,6 +194,7 @@ pub fn restore_header_from_recovery_key(
|
||||
wrapped_dek,
|
||||
header_nonce,
|
||||
header_tag,
|
||||
slots: vec![slot0],
|
||||
};
|
||||
|
||||
// 4. In Container schreiben
|
||||
|
||||
+717
-127
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,14 @@ pub fn red(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn magenta(s: &str) -> String {
|
||||
if is_vt_enabled() {
|
||||
format!("\x1b[1;35m{s}\x1b[0m")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
||||
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
||||
let tag = cyan(&format!("[{}/{}]", num, total));
|
||||
|
||||
+64
-67
@@ -388,12 +388,13 @@ pub struct SanctumFs {
|
||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||
format_version: u32,
|
||||
anti_leak: bool,
|
||||
vault_id: u32,
|
||||
last_activity: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl SanctumFs {
|
||||
pub fn new(db: Database, dek: Zeroizing<[u8; 32]>, format_version: u32) -> Self {
|
||||
Self::with_options(db, dek, format_version, true)
|
||||
Self::with_vault(db, dek, format_version, true, 0)
|
||||
}
|
||||
|
||||
pub fn with_options(
|
||||
@@ -401,6 +402,16 @@ impl SanctumFs {
|
||||
dek: Zeroizing<[u8; 32]>,
|
||||
format_version: u32,
|
||||
anti_leak: bool,
|
||||
) -> Self {
|
||||
Self::with_vault(db, dek, format_version, anti_leak, 0)
|
||||
}
|
||||
|
||||
pub fn with_vault(
|
||||
db: Database,
|
||||
dek: Zeroizing<[u8; 32]>,
|
||||
format_version: u32,
|
||||
anti_leak: bool,
|
||||
vault_id: u32,
|
||||
) -> Self {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -411,10 +422,15 @@ impl SanctumFs {
|
||||
dek: Arc::new(dek),
|
||||
format_version,
|
||||
anti_leak,
|
||||
vault_id,
|
||||
last_activity: Arc::new(AtomicU64::new(now)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vault_id(&self) -> u32 {
|
||||
self.vault_id
|
||||
}
|
||||
|
||||
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
||||
self.last_activity.clone()
|
||||
}
|
||||
@@ -442,6 +458,33 @@ impl SanctumFs {
|
||||
None => ("", trimmed),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_path(&self, path: &str) -> Result<Option<NodeRecord>, FsError> {
|
||||
self.db
|
||||
.resolve_path_in_vault(path, self.vault_id, &self.dek)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
}
|
||||
|
||||
fn list_children(&self, parent_id: i64) -> Result<Vec<NodeRecord>, FsError> {
|
||||
self.db
|
||||
.list_children_in_vault(parent_id, self.vault_id, &self.dek)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
}
|
||||
|
||||
fn create_node(&self, parent_id: i64, name: &str, is_dir: bool) -> Result<NodeRecord, FsError> {
|
||||
self.db
|
||||
.create_node_in_vault(self.vault_id, parent_id, name, is_dir, &self.dek)
|
||||
.map_err(|e| {
|
||||
error!("Fehler beim Erstellen des Knotens '{}': {e}", name);
|
||||
FsError::GeneralFailure
|
||||
})
|
||||
}
|
||||
|
||||
fn rename_node(&self, id: i64, new_parent_id: i64, new_name: &str) -> Result<(), FsError> {
|
||||
self.db
|
||||
.rename_node_in_vault(id, new_parent_id, new_name, self.vault_id, &self.dek)
|
||||
.map_err(|_| FsError::GeneralFailure)
|
||||
}
|
||||
}
|
||||
|
||||
impl DavFileSystem for SanctumFs {
|
||||
@@ -473,10 +516,7 @@ impl DavFileSystem for SanctumFs {
|
||||
self.touch();
|
||||
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
||||
|
||||
let existing_node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
let existing_node = self.resolve_path(&path_str)?;
|
||||
|
||||
let node = match existing_node {
|
||||
Some(n) => {
|
||||
@@ -511,21 +551,14 @@ impl DavFileSystem for SanctumFs {
|
||||
None => {
|
||||
if options.create || options.create_new {
|
||||
let parent = self
|
||||
.db
|
||||
.resolve_path(parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
self.db
|
||||
.create_node(parent.id, file_name, false)
|
||||
.map_err(|e| {
|
||||
error!("Fehler beim Erstellen der Datei: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?
|
||||
self.create_node(parent.id, file_name, false)?
|
||||
} else {
|
||||
return Err(FsError::NotFound);
|
||||
}
|
||||
@@ -552,19 +585,14 @@ impl DavFileSystem for SanctumFs {
|
||||
self.touch();
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
let children = self
|
||||
.db
|
||||
.list_children(node.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
let children = self.list_children(node.id)?;
|
||||
|
||||
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = children
|
||||
.into_iter()
|
||||
@@ -593,9 +621,7 @@ impl DavFileSystem for SanctumFs {
|
||||
self.touch();
|
||||
}
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
let meta = SanctumMetaData {
|
||||
@@ -623,28 +649,19 @@ impl DavFileSystem for SanctumFs {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
if self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.is_some()
|
||||
{
|
||||
if self.resolve_path(&path_str)?.is_some() {
|
||||
return Err(FsError::Exists);
|
||||
}
|
||||
|
||||
let parent = self
|
||||
.db
|
||||
.resolve_path(parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
self.db
|
||||
.create_node(parent.id, dir_name, true)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
self.create_node(parent.id, dir_name, true)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -655,15 +672,14 @@ impl DavFileSystem for SanctumFs {
|
||||
self.touch();
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
if node.id == 1 {
|
||||
let root_id = Database::get_root_node_id_for_vault(self.vault_id);
|
||||
if node.id == root_id {
|
||||
// Root-Verzeichnis darf nicht gelöscht werden
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
@@ -681,9 +697,7 @@ impl DavFileSystem for SanctumFs {
|
||||
self.touch();
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&path_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if node.is_dir {
|
||||
@@ -709,9 +723,7 @@ impl DavFileSystem for SanctumFs {
|
||||
let to_str = Self::path_to_str(to);
|
||||
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&from_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(&from_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
||||
@@ -721,9 +733,7 @@ impl DavFileSystem for SanctumFs {
|
||||
}
|
||||
|
||||
let to_parent = self
|
||||
.db
|
||||
.resolve_path(to_parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(to_parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !to_parent.is_dir {
|
||||
@@ -731,11 +741,7 @@ impl DavFileSystem for SanctumFs {
|
||||
}
|
||||
|
||||
// Falls Zieldatei bereits existiert und Datei ist: überschreiben / löschen
|
||||
if let Some(dest) = self
|
||||
.db
|
||||
.resolve_path(&to_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
{
|
||||
if let Some(dest) = self.resolve_path(&to_str)? {
|
||||
if dest.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
@@ -744,9 +750,7 @@ impl DavFileSystem for SanctumFs {
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
}
|
||||
|
||||
self.db
|
||||
.rename_node(node.id, to_parent.id, to_name)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
self.rename_node(node.id, to_parent.id, to_name)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -763,9 +767,7 @@ impl DavFileSystem for SanctumFs {
|
||||
let to_str = Self::path_to_str(to);
|
||||
|
||||
let node = self
|
||||
.db
|
||||
.resolve_path(&from_str)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(&from_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
if node.is_dir {
|
||||
@@ -779,15 +781,10 @@ impl DavFileSystem for SanctumFs {
|
||||
}
|
||||
|
||||
let to_parent = self
|
||||
.db
|
||||
.resolve_path(to_parent_path)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
.resolve_path(to_parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
|
||||
let dest_node = self
|
||||
.db
|
||||
.create_node(to_parent.id, to_name, false)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
let dest_node = self.create_node(to_parent.id, to_name, false)?;
|
||||
|
||||
// Kopiere alle Chunks und re-verschlüssele mit neuer node_id (wegen AAD-Bindung!)
|
||||
let total_chunks = if node.size == 0 {
|
||||
|
||||
Reference in New Issue
Block a user