Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
134 lines
4.5 KiB
Rust
134 lines
4.5 KiB
Rust
use assert_cmd::prelude::*;
|
|
use std::process::Command;
|
|
|
|
fn relicario(dir: &tempfile::TempDir) -> Command {
|
|
let mut cmd = Command::cargo_bin("relicario").unwrap();
|
|
cmd.current_dir(dir.path());
|
|
cmd
|
|
}
|
|
|
|
/// Full round-trip: init a vault with a key file, then add + get an item using
|
|
/// RELICARIO_KEYFILE.
|
|
#[test]
|
|
fn init_keyfile_then_unlock_keyfile_round_trips() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
// init with a key file
|
|
relicario(&dir)
|
|
.args(["init", "--key-file", "vault.relkey"])
|
|
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
|
.assert()
|
|
.success();
|
|
|
|
// add an item using the generated key file
|
|
relicario(&dir)
|
|
.args(["add", "login", "--title", "gh", "--username", "octocat", "--password", "hunter2"])
|
|
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
|
.env("RELICARIO_KEYFILE", dir.path().join("vault.relkey"))
|
|
.assert()
|
|
.success();
|
|
|
|
// get the item — distinctive username must appear in stdout
|
|
relicario(&dir)
|
|
.args(["get", "gh", "--show"])
|
|
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
|
.env("RELICARIO_KEYFILE", dir.path().join("vault.relkey"))
|
|
.assert()
|
|
.success()
|
|
.stdout(predicates::str::contains("octocat"));
|
|
}
|
|
|
|
/// Unit-level: init with --key-file writes the .relkey armor, records
|
|
/// second_factor=keyfile in params.json, and gitignores the key file.
|
|
#[test]
|
|
fn init_keyfile_writes_relkey_and_keyfile_params() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
relicario(&dir)
|
|
.args(["init", "--key-file", "vault.relkey"])
|
|
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
|
.assert()
|
|
.success();
|
|
|
|
// .relkey exists and is armored
|
|
let relkey = std::fs::read_to_string(dir.path().join("vault.relkey")).unwrap();
|
|
assert!(
|
|
relkey.starts_with("relicario-keyfile-v1\n"),
|
|
"relkey must be armored: {relkey}"
|
|
);
|
|
|
|
// params.json records the keyfile hint (parse — robust to pretty-print spacing)
|
|
let params: serde_json::Value = serde_json::from_str(
|
|
&std::fs::read_to_string(dir.path().join(".relicario/params.json")).unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(params["second_factor"], "keyfile");
|
|
|
|
// SECURITY: the in-the-clear .relkey must be gitignored
|
|
let gitignore = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
|
assert!(
|
|
gitignore.contains("vault.relkey"),
|
|
".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();
|
|
}
|