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<()> {