feat(cli): unlock resolves second factor from params hint (image|keyfile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
This commit is contained in:
adlee-was-taken
2026-06-25 21:37:59 -04:00
parent 6cb12990ec
commit e8f6635188
2 changed files with 85 additions and 11 deletions

View File

@@ -28,16 +28,28 @@ impl UnlockedVault {
pub fn root(&self) -> &Path { &self.root }
pub fn key(&self) -> &Zeroizing<[u8; 32]> { &self.master_key }
/// Full interactive unlock flow: locate vault, prompt passphrase, locate
/// reference image, derive master key.
/// Full interactive unlock flow: locate vault, prompt passphrase, resolve
/// the second factor (reference image or key file), derive master key.
pub fn unlock_interactive() -> Result<Self> {
let root = vault_dir()?;
let salt = read_salt(&root)?;
let params = read_params(&root)?;
let image_path = get_image_path()?;
let image_bytes = fs::read(&image_path)
.with_context(|| format!("failed to read reference image {}", image_path.display()))?;
let image_secret = Zeroizing::new(imgsecret::extract(&image_bytes)?);
let pf = read_params(&root)?;
let params = pf.to_kdf_params();
let secret: Zeroizing<[u8; 32]> = match pf.second_factor {
SecondFactor::Image => {
let image_path = get_image_path()?;
let image_bytes = fs::read(&image_path)
.with_context(|| format!("failed to read reference image {}", image_path.display()))?;
Zeroizing::new(imgsecret::extract(&image_bytes)?)
}
SecondFactor::Keyfile => {
let kf_path = get_keyfile_path()?;
let kf_bytes = fs::read(&kf_path)
.with_context(|| format!("failed to read key file {}", kf_path.display()))?;
relicario_core::keyfile::keyfile_decode(&kf_bytes).context("invalid key file")?
}
};
let passphrase = if let Some(p) = crate::test_passphrase_override() {
Zeroizing::new(p)
@@ -50,7 +62,7 @@ impl UnlockedVault {
let master_key = derive_master_key(
passphrase.as_bytes(),
&image_secret,
&secret,
&salt,
&params,
)?;
@@ -160,11 +172,10 @@ impl ParamsFile {
}
}
fn read_params(root: &Path) -> Result<KdfParams> {
fn read_params(root: &Path) -> Result<ParamsFile> {
let s = fs::read_to_string(root.join(".relicario").join("params.json"))
.context("failed to read .relicario/params.json")?;
let pf: ParamsFile = serde_json::from_str(&s).context("failed to parse params.json")?;
Ok(pf.to_kdf_params())
serde_json::from_str(&s).context("failed to parse params.json")
}
/// Locate the reference image path via `RELICARIO_IMAGE` env var or interactive prompt.
@@ -186,6 +197,29 @@ pub fn get_image_path() -> Result<PathBuf> {
Ok(PathBuf::from(trimmed))
}
/// Locate the key file via `RELICARIO_KEYFILE` env var, the
/// `<vault_root>/vault.relkey` convention, or an interactive prompt.
pub fn get_keyfile_path() -> Result<PathBuf> {
if let Ok(path) = std::env::var("RELICARIO_KEYFILE") {
return Ok(PathBuf::from(path));
}
if let Ok(root) = vault_dir() {
let default = root.join("vault.relkey");
if default.exists() {
return Ok(default);
}
}
eprint!("Key file path: ");
std::io::Write::flush(&mut std::io::stderr())?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
let trimmed = line.trim();
if trimmed.is_empty() {
bail!("no key file path provided");
}
Ok(PathBuf::from(trimmed))
}
/// Atomic write: write to <path>.tmp, then rename over <path>. Keeps the
/// vault file consistent if we crash mid-write.
fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {

View File

@@ -0,0 +1,40 @@
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. Ignored until Task 5 lands `init --key-file`.
#[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)
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", "u", "--password", "p"])
.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
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"));
}