use crate::base32;
pub fn build_uri(issuer: &str, account: &str, secret: &[u8]) -> String {
let label = format!("{}:{}", url_encode(issuer), url_encode(account));
let secret_b32 = base32::encode(secret);
let secret_b32 = secret_b32.trim_end_matches('=');
format!(
"otpauth://totp/{label}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30",
label = label,
secret = secret_b32,
issuer = url_encode(issuer),
)
}
fn url_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
let unreserved =
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~');
if unreserved {
out.push(b as char);
} else {
out.push('%');
out.push_str(&format!("{:02X}", b));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_inputs() {
let uri = build_uri("Lab10", "alice", b"12345678901234567890");
assert!(uri.starts_with("otpauth://totp/Lab10:alice?"));
assert!(uri.contains("secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
assert!(uri.contains("issuer=Lab10"));
assert!(uri.contains("algorithm=SHA1"));
assert!(uri.contains("digits=6"));
assert!(uri.contains("period=30"));
}
#[test]
fn encodes_special_chars_in_label() {
let uri = build_uri("My Co", "alice@example.com", b"secret___");
assert!(uri.contains("totp/My%20Co:alice%40example.com"));
assert!(uri.contains("issuer=My%20Co"));
}
}