//! `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(&crate::session::ParamsFile::for_new_vault(¶ms))?, )?; 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. crate::helpers::git_run(&root, &["init"], "init: git init")?; let _ = crate::helpers::git_command(&root, &[ "add", ".gitignore", ".relicario/params.json", ".relicario/salt", "manifest.enc", "settings.enc", ]).status()?; crate::helpers::git_run( &root, &["commit", "-m", "init: new Relicario vault (format v2)"], "init: git commit", )?; 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(()) }