feat(cli): init --key-file generates a .relkey second factor
Add `--key-file <path>` to `relicario init` as a mutually exclusive alternative to `--image`/`--output`. When given, a 32-byte secret is generated with OsRng, armored via `keyfile_encode`, written to the supplied path (gitignored, never committed), and `params.json` records `"second_factor": "keyfile"`. The `--image` arg is now Option<PathBuf>; existing image-vault callers are unchanged. `ParamsFile::for_new_vault` now takes the second-factor variant as an explicit argument. Tests: un-ignore init_keyfile_then_unlock_keyfile_round_trips (full round-trip), strengthen its assertion to check for distinctive username "octocat", and add init_keyfile_writes_relkey_and_keyfile_params (armor, params parse, gitignore). Full workspace green (0 failures), clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
This commit is contained in:
@@ -8,13 +8,12 @@ fn relicario(dir: &tempfile::TempDir) -> Command {
|
||||
}
|
||||
|
||||
/// Full round-trip: init a vault with a key file, then add + get an item using
|
||||
/// RELICARIO_KEYFILE. Ignored until Task 5 lands `init --key-file`.
|
||||
/// RELICARIO_KEYFILE.
|
||||
#[test]
|
||||
#[ignore = "un-ignored in Task 5 once init --key-file lands"]
|
||||
fn init_keyfile_then_unlock_keyfile_round_trips() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
// init with a key file (Task 5 wires the flag)
|
||||
// init with a key file
|
||||
relicario(&dir)
|
||||
.args(["init", "--key-file", "vault.relkey"])
|
||||
.env("RELICARIO_TEST_PASSPHRASE", "correct horse")
|
||||
@@ -23,18 +22,51 @@ fn init_keyfile_then_unlock_keyfile_round_trips() {
|
||||
|
||||
// add an item using the generated key file
|
||||
relicario(&dir)
|
||||
.args(["add", "login", "--title", "gh", "--username", "u", "--password", "p"])
|
||||
.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 — username "u" must appear in stdout
|
||||
// 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("u"));
|
||||
.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}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user