diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 33d20a6..edca410 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -7,7 +7,7 @@ mod session; use std::path::PathBuf; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; #[derive(Parser)] @@ -256,7 +256,94 @@ fn main() -> Result<()> { } } -fn cmd_init(_image: PathBuf, _output: PathBuf) -> Result<()> { bail!("not yet implemented"); } +fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> { + use std::fs; + use rand::{rngs::OsRng, RngCore}; + use relicario_core::{ + derive_master_key, encrypt_manifest, encrypt_settings, imgsecret, + validate_passphrase_strength, KdfParams, Manifest, VaultSettings, + }; + use zeroize::Zeroizing; + + let root = std::env::current_dir()?; + let relicario_dir = root.join(".relicario"); + if relicario_dir.exists() { + anyhow::bail!(".relicario/ already exists in {}", root.display()); + } + + // Passphrase with strength gate (audit H3). + let passphrase = Zeroizing::new(rpassword::prompt_password("Choose a passphrase: ")?); + let confirm = Zeroizing::new(rpassword::prompt_password("Confirm passphrase: ")?); + if passphrase.as_str() != confirm.as_str() { + anyhow::bail!("passphrases do not match"); + } + if let Err(e) = validate_passphrase_strength(&passphrase) { + anyhow::bail!("{}. Choose a longer or more entropic phrase.", e); + } + + // Image secret: 32 random bytes, embedded in the carrier. + let mut image_secret = [0u8; 32]; + OsRng.fill_bytes(&mut image_secret); + let carrier = fs::read(&image) + .with_context(|| format!("failed to read carrier image {}", image.display()))?; + let stego = imgsecret::embed(&carrier, &image_secret)?; + fs::write(&output, &stego) + .with_context(|| format!("failed to write reference image {}", output.display()))?; + + // Vault salt + KDF params. + let mut salt = [0u8; 32]; + OsRng.fill_bytes(&mut salt); + let params = KdfParams { argon2_m: 65536, argon2_t: 3, argon2_p: 4 }; + + // Derive master key, then persist an empty Manifest + default VaultSettings. + let master_key = derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)?; + + fs::create_dir_all(&relicario_dir)?; + fs::create_dir_all(root.join("items"))?; + fs::create_dir_all(root.join("attachments"))?; + fs::write(relicario_dir.join("salt"), salt)?; + fs::write( + relicario_dir.join("params.json"), + serde_json::to_string_pretty(&ParamsFile { + format_version: 2, + kdf: ParamsKdf { + algorithm: "argon2id-v0x13".into(), + argon2_m: params.argon2_m, + argon2_t: params.argon2_t, + argon2_p: params.argon2_p, + }, + aead: "xchacha20poly1305".into(), + salt_path: ".relicario/salt".into(), + })?, + )?; + fs::write(relicario_dir.join("devices.json"), b"[]")?; + + let manifest = Manifest::new(); + fs::write(root.join("manifest.enc"), encrypt_manifest(&manifest, &master_key)?)?; + let settings = VaultSettings::default(); + fs::write(root.join("settings.enc"), encrypt_settings(&settings, &master_key)?)?; + + // .gitignore excludes the reference image. + let gitignore = format!("{}\n", output.file_name().unwrap().to_string_lossy()); + fs::write(root.join(".gitignore"), gitignore)?; + + // git init + initial commit via hardened wrapper. + let status = crate::helpers::git_command(&root, &["init"]).status()?; + if !status.success() { anyhow::bail!("git init failed"); } + let _ = crate::helpers::git_command(&root, &[ + "add", ".gitignore", ".relicario/params.json", ".relicario/devices.json", + "manifest.enc", "settings.enc", + ]).status()?; + let status = crate::helpers::git_command(&root, &[ + "commit", "-m", "init: new relicario vault (format v2)", + ]).status()?; + if !status.success() { anyhow::bail!("git commit failed"); } + + eprintln!("Vault initialized at {}", root.display()); + eprintln!("Reference image: {}", output.display()); + eprintln!(" \u{2192} back this file up somewhere safe; it is your second factor."); + Ok(()) +} fn cmd_add(_kind: AddKind) -> Result<()> { bail!("not yet implemented"); } fn cmd_get(_query: String, _show: bool, _copy: bool) -> Result<()> { bail!("not yet implemented"); } fn cmd_list(_t: Option, _g: Option, _tag: Option, _trashed: bool) -> Result<()> { bail!("not yet implemented"); } @@ -272,3 +359,20 @@ fn cmd_generate(_l: u32, _b: bool, _w: u32, _s: String, _sep: String) -> Result< fn cmd_settings(_a: SettingsAction) -> Result<()> { bail!("not yet implemented"); } fn cmd_sync() -> Result<()> { bail!("not yet implemented"); } fn cmd_device(_a: DeviceAction) -> Result<()> { bail!("not yet implemented"); } + +#[derive(serde::Serialize)] +struct ParamsFile { + format_version: u32, + kdf: ParamsKdf, + aead: String, + salt_path: String, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct ParamsKdf { + algorithm: String, + argon2_m: u32, + argon2_t: u32, + argon2_p: u32, +}