pub const UNAMBIGUOUS_CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ23456789";
pub const AP_PASSPHRASE_LEN: usize = 12;
const _: () = assert!(
AP_PASSPHRASE_LEN >= 8 && AP_PASSPHRASE_LEN <= 63,
"the AP passphrase length must be legal for WPA2-PSK"
);
pub fn generate(len: usize) -> Result<String, getrandom::Error> {
let n = UNAMBIGUOUS_CHARSET.len() as u8;
let limit = (256u16 - (256u16 % n as u16)) as u8;
let mut out = String::with_capacity(len);
while out.len() < len {
let mut b = [0u8; 1];
getrandom::fill(&mut b)?;
if b[0] < limit {
out.push(UNAMBIGUOUS_CHARSET[(b[0] % n) as usize] as char);
}
}
Ok(out)
}
pub fn generate_ap_passphrase() -> Result<String, getrandom::Error> {
generate(AP_PASSPHRASE_LEN)
}
pub fn is_valid_wpa2_passphrase(s: &str) -> bool {
let len = s.chars().count();
(8..=63).contains(&len) && s.chars().all(|c| c.is_ascii_graphic() || c == ' ')
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn a_generated_passphrase_is_legal_for_wpa2() {
let p = generate_ap_passphrase().expect("entropy");
assert!(
is_valid_wpa2_passphrase(&p),
"hostapd refuses the whole config on a bad passphrase: {p:?}"
);
assert_eq!(p.chars().count(), AP_PASSPHRASE_LEN);
}
#[test]
fn hex_of_32_bytes_would_have_been_illegal() {
assert!(!is_valid_wpa2_passphrase(&"a".repeat(64)));
assert!(is_valid_wpa2_passphrase(&"a".repeat(63)));
}
#[test]
fn only_unambiguous_characters_are_used() {
for _ in 0..200 {
let p = generate(16).expect("entropy");
for c in p.chars() {
assert!(
!"0O1IL".contains(c),
"ambiguous character {c:?} in generated secret {p:?}"
);
assert!(UNAMBIGUOUS_CHARSET.contains(&(c as u8)));
}
}
}
#[test]
fn successive_draws_differ() {
let mut seen = HashSet::new();
for _ in 0..50 {
assert!(
seen.insert(generate_ap_passphrase().expect("entropy")),
"a repeated passphrase means the draw is not random"
);
}
}
#[test]
fn every_charset_position_is_reachable() {
let mut seen = HashSet::new();
for _ in 0..400 {
for c in generate(32).expect("entropy").chars() {
seen.insert(c);
}
}
assert_eq!(
seen.len(),
UNAMBIGUOUS_CHARSET.len(),
"some characters are never drawn, so the sampling is biased"
);
}
#[test]
fn a_zero_length_draw_is_empty_rather_than_looping() {
assert_eq!(generate(0).expect("entropy"), "");
}
#[test]
fn validation_rejects_the_obvious_bad_shapes() {
assert!(!is_valid_wpa2_passphrase(""), "empty");
assert!(!is_valid_wpa2_passphrase("short"), "under 8");
assert!(
!is_valid_wpa2_passphrase("with\ttab\tchars"),
"control chars"
);
assert!(!is_valid_wpa2_passphrase("café-passphrase"), "non-ascii");
assert!(is_valid_wpa2_passphrase("a valid one"), "spaces are legal");
}
}