cli: --totp-qr <path> flag on add login + edit (rqrr decode)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-01 22:22:20 -04:00
parent bd8102c9ad
commit 8855078179
5 changed files with 224 additions and 10 deletions

View File

@@ -115,6 +115,34 @@ pub fn write_groups_cache(
std::fs::write(path, body)
}
/// Decode a QR image at `path`. Returns the otpauth secret (base32) if the
/// QR decodes to an `otpauth://...` URI with a `secret` query param.
pub fn decode_totp_qr(path: &std::path::Path) -> anyhow::Result<String> {
let img = image::open(path)
.map_err(|e| anyhow::anyhow!("failed to read image: {e}"))?
.to_luma8();
let mut prepared = rqrr::PreparedImage::prepare(img);
let grids = prepared.detect_grids();
let grid = grids
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("no QR code found in image"))?;
let (_meta, content) = grid
.decode()
.map_err(|e| anyhow::anyhow!("QR decode failed: {e}"))?;
if !content.starts_with("otpauth://") {
return Err(anyhow::anyhow!("not a TOTP URI (expected otpauth://...)"));
}
let parsed =
url::Url::parse(&content).map_err(|e| anyhow::anyhow!("invalid otpauth URI: {e}"))?;
let secret = parsed
.query_pairs()
.find(|(k, _)| k == "secret")
.map(|(_, v)| v.to_string())
.ok_or_else(|| anyhow::anyhow!("otpauth URI missing `secret` parameter"))?;
Ok(secret)
}
#[cfg(test)]
mod tests {
use super::*;