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:
@@ -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,
|
||||
¶ms,
|
||||
)?;
|
||||
@@ -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<()> {
|
||||
|
||||
Reference in New Issue
Block a user