refactor(core): extract base32 module, dedupe two RFC 4648 impls

New crates/relicario-core/src/base32.rs hosts encode_rfc4648 +
decode_rfc4648_lenient (case-insensitive, optional padding, whitespace
stripped). Folds inline base32_encode (item.rs:255-275) and
decode_base32_totp (import_lastpass.rs:202-220) into the shared module;
both call sites updated.

- New RelicarioError::InvalidBase32(String) variant for the decoder
  error path
- Module is pub(crate); public API surface unchanged
- Steam alphabet (item_types/totp.rs:13) intentionally separate with
  neighbour comment pointing at crate::base32

Plan B Phase 7 sub-step 1 (DEV-A P2 base32 dedup half).
docs/superpowers/specs/2026-05-04-cli-restructure-design.md.

cargo test --workspace: green
cargo clippy --workspace: silent

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-09 10:44:16 -04:00
parent b9bd152e9d
commit e5d63ab196
6 changed files with 146 additions and 47 deletions

View File

@@ -158,8 +158,8 @@ fn map_row(
let totp = if totp_raw.is_empty() {
None
} else {
match decode_base32_totp(totp_raw) {
Some(bytes) if !bytes.is_empty() => Some(crate::item_types::TotpConfig {
match crate::base32::decode_rfc4648_lenient(totp_raw) {
Ok(bytes) if !bytes.is_empty() => Some(crate::item_types::TotpConfig {
secret: Zeroizing::new(bytes),
algorithm: crate::item_types::TotpAlgorithm::Sha1,
digits: 6,
@@ -196,25 +196,3 @@ fn map_row(
(Some(item), warning)
}
/// Decode a base32-encoded TOTP secret per RFC 4648, case-insensitive,
/// padding optional. Returns None if the input contains any non-alphabet
/// character (after upper-casing). Used by the LastPass importer.
fn decode_base32_totp(secret: &str) -> Option<Vec<u8>> {
const ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let upper = secret.trim().trim_end_matches('=').to_ascii_uppercase();
if upper.is_empty() { return None; }
let mut out = Vec::with_capacity(upper.len() * 5 / 8);
let mut buffer: u32 = 0;
let mut bits: u32 = 0;
for ch in upper.bytes() {
let idx = ALPHA.iter().position(|&a| a == ch)?;
buffer = (buffer << 5) | (idx as u32);
bits += 5;
if bits >= 8 {
bits -= 8;
out.push(((buffer >> bits) & 0xFF) as u8);
}
}
Some(out)
}