use ring::rand::{SecureRandom, SystemRandom};
use crate::admin::totp::BASE32_ALPHABET;
pub const CODE_COUNT: usize = 10;
pub const CODE_LEN: usize = 10;
const GROUP_LEN: usize = 5;
#[must_use]
pub fn generate_codes() -> Vec<String> {
(0..CODE_COUNT).map(|_| generate_code()).collect()
}
#[must_use]
fn generate_code() -> String {
let mut bytes = [0u8; CODE_LEN];
SystemRandom::new()
.fill(&mut bytes)
.expect("system RNG unavailable");
let mut code = String::with_capacity(CODE_LEN + 1);
for (index, byte) in bytes.iter().enumerate() {
if index > 0 && index % GROUP_LEN == 0 {
code.push('-');
}
code.push(BASE32_ALPHABET[usize::from(*byte) % BASE32_ALPHABET.len()] as char);
}
code
}
#[must_use]
pub fn normalize(raw: &str) -> String {
raw.chars()
.filter(|character| character.is_ascii_alphanumeric())
.map(|character| character.to_ascii_uppercase())
.collect()
}
#[must_use]
pub fn is_well_formed(candidate: &str) -> bool {
candidate.len() == CODE_LEN
&& candidate
.bytes()
.all(|byte| BASE32_ALPHABET.contains(&byte))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn a_set_is_ten_distinct_codes_from_the_base32_alphabet() {
let codes = generate_codes();
assert_eq!(codes.len(), CODE_COUNT);
let distinct: HashSet<&String> = codes.iter().collect();
assert_eq!(
distinct.len(),
CODE_COUNT,
"codes must not repeat: {codes:?}"
);
for code in &codes {
assert_eq!(code.len(), CODE_LEN + 1, "{code}");
assert_eq!(code.chars().nth(GROUP_LEN), Some('-'), "{code}");
let normalized = normalize(code);
assert!(
is_well_formed(&normalized),
"a freshly minted code must pass the shape check: {code}"
);
}
}
#[test]
fn normalize_absorbs_the_ways_a_human_retypes_a_code() {
let canonical = "K7QF23BXTM";
for typed in [
"K7QF2-3BXTM",
"k7qf2-3bxtm",
" K7QF2 3BXTM ",
"K7QF2\t3BXTM\n",
"K7QF23BXTM",
"K7-QF2-3B-XTM",
] {
assert_eq!(normalize(typed), canonical, "input {typed:?}");
}
assert_eq!(normalize(canonical), canonical);
assert_eq!(normalize(&normalize("k7qf2-3bxtm")), canonical);
}
#[test]
fn the_shape_check_refuses_what_is_plainly_not_a_recovery_code() {
assert!(is_well_formed("K7QF23BXTM"));
let cases = [
("empty", ""),
("a totp code", "123456"),
("too short", "K7QF23BXT"),
("too long", "K7QF23BXTMM"),
("contains 0", "K7QF23BXT0"),
("contains 1", "K7QF23BXT1"),
("contains 8", "K7QF23BXT8"),
("contains 9", "K7QF23BXT9"),
("still grouped", "K7QF2-3BXT"),
("lowercase", "k7qf23bxtm"),
];
for (name, candidate) in cases {
assert!(!is_well_formed(candidate), "case `{name}`");
}
}
}