diff --git a/crates/relicario-cli/src/commands/init.rs b/crates/relicario-cli/src/commands/init.rs index ec974f5..e4ee0da 100644 --- a/crates/relicario-cli/src/commands/init.rs +++ b/crates/relicario-cli/src/commands/init.rs @@ -48,7 +48,7 @@ pub fn cmd_init(image: Option, output: PathBuf, key_file: Option { // Key-file mode: write the armored secret in the clear. // SECURITY: this file holds the 256-bit secret in the clear and @@ -63,7 +63,7 @@ pub fn cmd_init(image: Option, output: PathBuf, key_file: Option { // Image mode: embed into a carrier JPEG, write the reference image. @@ -79,7 +79,7 @@ pub fn cmd_init(image: Option, output: PathBuf, key_file: Option { anyhow::bail!( @@ -133,11 +133,11 @@ pub fn cmd_init(image: Option, output: PathBuf, key_file: Option { - eprintln!("Key file: {gitignore_name}"); + eprintln!("Key file: {container_display}"); eprintln!(" \u{2192} back this file up somewhere safe; it is your second factor."); } SecondFactor::Image => { - eprintln!("Reference image: {}", output.display()); + eprintln!("Reference image: {container_display}"); eprintln!(" \u{2192} back this file up somewhere safe; it is your second factor."); } } diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 3ff8f89..21167d5 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -35,7 +35,7 @@ enum Commands { /// Carrier JPEG to embed the secret into (image second factor). #[arg(long)] image: Option, - /// Output path for the reference image (gitignored). + /// Output path for the reference image (gitignored); ignored when --key-file is used. #[arg(long, default_value = "reference.jpg")] output: PathBuf, /// Generate a key-file second factor at this path instead of a reference image (gitignored). diff --git a/crates/relicario-cli/tests/keyfile_flows.rs b/crates/relicario-cli/tests/keyfile_flows.rs index 5369b73..6608938 100644 --- a/crates/relicario-cli/tests/keyfile_flows.rs +++ b/crates/relicario-cli/tests/keyfile_flows.rs @@ -70,3 +70,64 @@ fn init_keyfile_writes_relkey_and_keyfile_params() { ".relkey must be gitignored: {gitignore}" ); } + +/// SECURITY INVARIANT: the in-the-clear .relkey second factor must NEVER enter +/// the git tree — not in the init commit, and not even after an aggressive `git add -A`. +/// The remote/server must only ever receive opaque ciphertext, never the secret. +#[test] +fn relkey_secret_never_enters_git_tree() { + let dir = tempfile::tempdir().unwrap(); + relicario(&dir) + .args(["init", "--key-file", "vault.relkey"]) + .env("RELICARIO_TEST_PASSPHRASE", "correct horse") + .assert() + .success(); + + let git_tracked = |label: &str| { + let out = Command::new("git") + .current_dir(dir.path()) + .args(["ls-files"]) + .output() + .expect("run git ls-files"); + let files = String::from_utf8_lossy(&out.stdout).into_owned(); + assert!( + !files.contains("vault.relkey"), + "{label}: key file leaked into git tree:\n{files}" + ); + }; + + // Not in the init commit. + git_tracked("after init commit"); + + // Survives an aggressive add -A (gitignore must hold). + let status = Command::new("git") + .current_dir(dir.path()) + .args(["add", "-A"]) + .status() + .expect("run git add -A"); + assert!(status.success(), "git add -A failed"); + git_tracked("after git add -A"); +} + +/// init with no second factor must fail with a clear, actionable error. +#[test] +fn init_without_a_factor_fails() { + let dir = tempfile::tempdir().unwrap(); + relicario(&dir) + .args(["init"]) + .env("RELICARIO_TEST_PASSPHRASE", "correct horse") + .assert() + .failure() + .stderr(predicates::str::contains("either --image")); +} + +/// --image and --key-file are mutually exclusive (clap rejects before the handler). +#[test] +fn init_with_both_factors_fails() { + let dir = tempfile::tempdir().unwrap(); + relicario(&dir) + .args(["init", "--image", "carrier.jpg", "--key-file", "vault.relkey"]) + .env("RELICARIO_TEST_PASSPHRASE", "correct horse") + .assert() + .failure(); +}