fix(cli): resolve second factor via shared helper for recovery-qr + backup (key-file parity); wasm equivalence negative control

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
This commit is contained in:
adlee-was-taken
2026-06-26 00:11:50 -04:00
parent 23c8bbea65
commit 2203d817ec
5 changed files with 75 additions and 31 deletions

View File

@@ -36,20 +36,7 @@ impl UnlockedVault {
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 secret = resolve_second_factor_secret(&root, pf.second_factor)?;
let passphrase = if let Some(p) = crate::test_passphrase_override() {
Zeroizing::new(p)
@@ -172,7 +159,7 @@ impl ParamsFile {
}
}
fn read_params(root: &Path) -> Result<ParamsFile> {
pub(crate) 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")?;
serde_json::from_str(&s).context("failed to parse params.json")
@@ -220,6 +207,29 @@ pub fn get_keyfile_path() -> Result<PathBuf> {
Ok(PathBuf::from(trimmed))
}
/// Resolve the vault's 32-byte second-factor secret per the params hint:
/// extract it from the reference image, or decode it from the key file.
pub(crate) fn resolve_second_factor_secret(
root: &Path,
second_factor: SecondFactor,
) -> Result<Zeroizing<[u8; 32]>> {
let _ = root; // retained in the API for future vault-relative resolution
match 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()))?;
Ok(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")
}
}
}
/// 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<()> {