#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrengthLevel {
VeryWeak,
Weak,
Fair,
Strong,
VeryStrong,
}
impl StrengthLevel {
pub fn label(self) -> &'static str {
match self {
StrengthLevel::VeryWeak => "very weak",
StrengthLevel::Weak => "weak",
StrengthLevel::Fair => "fair",
StrengthLevel::Strong => "strong",
StrengthLevel::VeryStrong => "very strong",
}
}
pub fn is_weak(self) -> bool {
matches!(self, StrengthLevel::VeryWeak | StrengthLevel::Weak)
}
}
pub fn estimate_entropy_bits(passphrase: &str) -> f64 {
let len = passphrase.chars().count();
if len == 0 {
return 0.0;
}
let has_lower = passphrase.chars().any(|c| c.is_ascii_lowercase());
let has_upper = passphrase.chars().any(|c| c.is_ascii_uppercase());
let has_digit = passphrase.chars().any(|c| c.is_ascii_digit());
let has_symbol = passphrase
.chars()
.any(|c| c.is_ascii() && !c.is_ascii_alphanumeric() && c != ' ');
let has_space = passphrase.contains(' ');
let has_non_ascii = !passphrase.is_ascii();
let mut charset_size: u32 = 0;
if has_lower {
charset_size += 26;
}
if has_upper {
charset_size += 26;
}
if has_digit {
charset_size += 10;
}
if has_symbol {
charset_size += 32;
}
if has_space {
charset_size += 1;
}
if has_non_ascii {
charset_size += 100;
}
if charset_size == 0 {
return 0.0;
}
(len as f64) * (charset_size as f64).log2()
}
pub fn classify(bits: f64) -> StrengthLevel {
if bits < 28.0 {
StrengthLevel::VeryWeak
} else if bits < 40.0 {
StrengthLevel::Weak
} else if bits < 60.0 {
StrengthLevel::Fair
} else if bits < 80.0 {
StrengthLevel::Strong
} else {
StrengthLevel::VeryStrong
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_passphrase_has_zero_entropy() {
assert_eq!(estimate_entropy_bits(""), 0.0);
assert_eq!(classify(0.0), StrengthLevel::VeryWeak);
}
#[test]
fn short_lowercase_only_is_weak_or_worse() {
let bits = estimate_entropy_bits("hunter2".trim_end_matches(char::is_numeric));
assert!(classify(bits).is_weak(), "bits = {bits}");
}
#[test]
fn long_mixed_charset_is_strong() {
let bits = estimate_entropy_bits("Tr0ub4dor&3-XyZ!qw");
assert!(
matches!(
classify(bits),
StrengthLevel::Strong | StrengthLevel::VeryStrong
),
"bits = {bits}"
);
}
#[test]
fn longer_passphrase_scores_higher_than_shorter_with_same_charset() {
let short = estimate_entropy_bits("abcabc");
let long = estimate_entropy_bits("abcabcabcabc");
assert!(long > short);
}
#[test]
fn classify_boundaries() {
assert_eq!(classify(27.9), StrengthLevel::VeryWeak);
assert_eq!(classify(28.0), StrengthLevel::Weak);
assert_eq!(classify(39.9), StrengthLevel::Weak);
assert_eq!(classify(40.0), StrengthLevel::Fair);
assert_eq!(classify(59.9), StrengthLevel::Fair);
assert_eq!(classify(60.0), StrengthLevel::Strong);
assert_eq!(classify(79.9), StrengthLevel::Strong);
assert_eq!(classify(80.0), StrengthLevel::VeryStrong);
}
}