Initial commit: Sanctum encrypted single-file container for Windows
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams};
|
||||
use sanctum::mount::{format_drive, mount_container, unmount_drive};
|
||||
use sanctum::storage::Database;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "sanctum")]
|
||||
#[command(author = "Sanctum Systems & Security Engineering")]
|
||||
#[command(about = "Sanctum: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland", long_about = None)]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialisiert einen neuen verschlüsselten Sanctum-Container (.sanctum)
|
||||
Init {
|
||||
/// Pfad zur zu erstellenden .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Bindet einen Sanctum-Container als Windows-Netzlaufwerk via WebDAV ein
|
||||
Mount {
|
||||
/// Pfad zur .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:')
|
||||
#[arg(short, long)]
|
||||
drive: String,
|
||||
|
||||
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
},
|
||||
|
||||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||
Unmount {
|
||||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:')
|
||||
#[arg(short, long)]
|
||||
drive: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_drive_letter(s: &str) -> Result<char> {
|
||||
let trimmed = s.trim();
|
||||
let ch = trimmed
|
||||
.chars()
|
||||
.next()
|
||||
.context("Laufwerksangabe darf nicht leer sein")?;
|
||||
|
||||
if !ch.is_ascii_alphabetic() {
|
||||
bail!("Ungültiger Laufwerksbuchstabe: '{}'", trimmed);
|
||||
}
|
||||
|
||||
Ok(ch.to_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn handle_init(container_path: &Path) -> Result<()> {
|
||||
if container_path.exists() {
|
||||
bail!(
|
||||
"Zieldatei '{}' existiert bereits. Initialisierung abgebrochen, um Überschreiben zu verhindern.",
|
||||
container_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
println!("============================================================");
|
||||
println!(" Sanctum Container Initialisierung");
|
||||
println!(" Zieldatei: {}", container_path.display());
|
||||
println!("============================================================");
|
||||
|
||||
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!");
|
||||
}
|
||||
|
||||
info!("Generiere KDF-Salt und leite KEK via Argon2id ab ...");
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams::default();
|
||||
let kek = derive_kek(&password, &salt, &kdf_params)
|
||||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||||
|
||||
info!("Erzeuge kryptografisch sicheren DEK via CSPRNG (OsRng) ...");
|
||||
let dek = generate_dek();
|
||||
|
||||
info!("Verschlüssele DEK mit KEK via AES-256-GCM ...");
|
||||
let (wrapped_dek, header_nonce, header_tag) =
|
||||
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
||||
|
||||
info!("Initialisiere SQLite-Containerstruktur mit 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")?;
|
||||
|
||||
println!();
|
||||
println!("✔ Sanctum-Container erfolgreich initialisiert!");
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!(" Format: Version 1 (Magic: SANCTUM\\0)");
|
||||
println!(" KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||
println!(" Cipher: AES-256-GCM mit AEAD-Swap-Protection (1-MB Chunks)");
|
||||
println!();
|
||||
println!("Zum Einbinden ausführen:");
|
||||
println!(
|
||||
" sanctum mount --path \"{}\" --drive S",
|
||||
container_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Tracing / Logging initialisieren (Standard-Filter: info)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("sanctum=info,dav_server=warn,hyper=warn")),
|
||||
)
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Init { path } => {
|
||||
handle_init(&path)?;
|
||||
}
|
||||
Commands::Mount { path, drive, port } => {
|
||||
let drive_char = parse_drive_letter(&drive)?;
|
||||
let password = rpassword::prompt_password(format!(
|
||||
"Master-Passwort für Container '{}' eingeben: ",
|
||||
path.display()
|
||||
))
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
|
||||
mount_container(&path, drive_char, port, &password).await?;
|
||||
}
|
||||
Commands::Unmount { drive } => {
|
||||
let drive_char = parse_drive_letter(&drive)?;
|
||||
unmount_drive(drive_char)?;
|
||||
println!("✔ Laufwerk {} erfolgreich getrennt.", format_drive(drive_char));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user