From b9b07ec68d75af586c9845e4d99bd1c1cf43e796 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Wed, 6 May 2026 18:32:36 -0400 Subject: [PATCH] refactor(cli): move cmd_init into commands/init.rs (carries inline ParamsFile) --- crates/relicario-cli/src/commands/init.rs | 125 ++++++++++++++++++++++ crates/relicario-cli/src/commands/mod.rs | 1 + crates/relicario-cli/src/main.rs | 120 +-------------------- 3 files changed, 127 insertions(+), 119 deletions(-) create mode 100644 crates/relicario-cli/src/commands/init.rs diff --git a/crates/relicario-cli/src/commands/init.rs b/crates/relicario-cli/src/commands/init.rs new file mode 100644 index 0000000..f0c49da --- /dev/null +++ b/crates/relicario-cli/src/commands/init.rs @@ -0,0 +1,125 @@ +//! `relicario init` — bootstrap a fresh vault in the current directory. + +use std::path::PathBuf; + +use anyhow::{Context, Result}; + +pub 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). + // RELICARIO_TEST_PASSPHRASE is a test-only escape hatch that bypasses the + // TTY prompt so integration tests can run without a real TTY. + let passphrase = if let Some(p) = crate::test_passphrase_override() { + Zeroizing::new(p) + } else { + Zeroizing::new(rpassword::prompt_password("Choose a passphrase: ")?) + }; + let confirm = if crate::test_passphrase_override().is_some() { + passphrase.clone() + } else { + 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 image_secret = { + let mut buf = Zeroizing::new([0u8; 32]); + OsRng.fill_bytes(buf.as_mut_slice()); + buf + }; + 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(), + })?, + )?; + 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 fname = output.file_name() + .ok_or_else(|| anyhow::anyhow!("output path has no filename: {}", output.display()))? + .to_string_lossy(); + let gitignore = format!("{fname}\n"); + 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/salt", "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(()) +} + +#[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, +} diff --git a/crates/relicario-cli/src/commands/mod.rs b/crates/relicario-cli/src/commands/mod.rs index 38c1cdc..427eb0b 100644 --- a/crates/relicario-cli/src/commands/mod.rs +++ b/crates/relicario-cli/src/commands/mod.rs @@ -7,4 +7,5 @@ //! `use crate::commands::*`. pub mod generate; +pub mod init; pub mod rate; diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index e67872b..48e18d3 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -426,7 +426,7 @@ enum RecoveryQrCmd { fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Init { image, output } => cmd_init(image, output), + Commands::Init { image, output } => commands::init::cmd_init(image, output), Commands::Add { kind } => cmd_add(kind), Commands::Get { query, show, copy } => cmd_get(query, show, copy), Commands::List { r#type, group, tag, trashed } => cmd_list(r#type, group, tag, trashed), @@ -508,108 +508,6 @@ pub(crate) fn test_backup_passphrase_override() -> Option { None } -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). - // RELICARIO_TEST_PASSPHRASE is a test-only escape hatch that bypasses the - // TTY prompt so integration tests can run without a real TTY. - let passphrase = if let Some(p) = test_passphrase_override() { - Zeroizing::new(p) - } else { - Zeroizing::new(rpassword::prompt_password("Choose a passphrase: ")?) - }; - let confirm = if test_passphrase_override().is_some() { - passphrase.clone() - } else { - 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 image_secret = { - let mut buf = Zeroizing::new([0u8; 32]); - OsRng.fill_bytes(buf.as_mut_slice()); - buf - }; - 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(), - })?, - )?; - 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 fname = output.file_name() - .ok_or_else(|| anyhow::anyhow!("output path has no filename: {}", output.display()))? - .to_string_lossy(); - let gitignore = format!("{fname}\n"); - 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/salt", "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<()> { let vault = crate::session::UnlockedVault::unlock_interactive()?; let mut manifest = vault.load_manifest()?; @@ -2129,22 +2027,6 @@ fn cmd_status() -> Result<()> { println!("Last export: {last_backup_str}"); Ok(()) } -#[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, -} // ── Device management ─────────────────────────────────────────────────────────