pub struct StrengthWarning(pub String);
pub const MIN_ENTROPY_BITS: f64 = 50.0;
pub fn entropy_bits(passphrase: &str) -> f64 {
let mut pool = 0u32;
if passphrase.chars().any(|c| c.is_ascii_lowercase()) {
pool += 26;
}
if passphrase.chars().any(|c| c.is_ascii_uppercase()) {
pool += 26;
}
if passphrase.chars().any(|c| c.is_ascii_digit()) {
pool += 10;
}
if passphrase
.chars()
.any(|c| c.is_ascii_punctuation() || c == ' ')
{
pool += 33;
}
if !passphrase.is_ascii() {
pool += 100; }
if pool == 0 {
return 0.0;
}
passphrase.chars().count() as f64 * (pool as f64).log2()
}
pub fn meets_floor(passphrase: &str) -> Result<(), String> {
let bits = entropy_bits(passphrase);
if bits < MIN_ENTROPY_BITS {
return Err(format!(
"passphrase is too weak (~{bits:.0} bits, need {MIN_ENTROPY_BITS:.0}) — \
use a longer or more varied passphrase, or pass --force to override"
));
}
Ok(())
}
pub fn check(passphrase: &str) -> Option<StrengthWarning> {
if passphrase.len() < 12 {
return Some(StrengthWarning(
"Passphrase is short (< 12 chars). A weak passphrase is the easiest attack vector."
.into(),
));
}
let has_upper = passphrase.chars().any(|c| c.is_uppercase());
let has_lower = passphrase.chars().any(|c| c.is_lowercase());
let has_digit = passphrase.chars().any(|c| c.is_ascii_digit());
let has_symbol = passphrase.chars().any(|c| !c.is_alphanumeric());
let variety = [has_upper, has_lower, has_digit, has_symbol]
.iter()
.filter(|&&v| v)
.count();
if variety < 2 {
return Some(StrengthWarning(
"Passphrase uses only one character type. Mix letters, numbers, and symbols.".into(),
));
}
let common = [
"password",
"passphrase",
"secret",
"admin",
"svault",
"123456",
"qwerty",
];
let lower = passphrase.to_lowercase();
if common.iter().any(|w| lower.contains(w)) {
return Some(StrengthWarning(
"Passphrase contains a common word. Choose something less predictable.".into(),
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strong_passphrase_passes() {
assert!(check("Str0ng!Phrase#99").is_none());
}
#[test]
fn short_passphrase_warns() {
assert!(check("ab1!").is_some());
}
#[test]
fn single_character_type_warns() {
assert!(check("abcdefghijklmnop").is_some());
}
#[test]
fn common_word_warns() {
assert!(check("MyPassword123!").is_some());
}
#[test]
fn entropy_floor_rejects_weak_and_accepts_strong() {
assert!(meets_floor("abcdef").is_err());
assert!(meets_floor("Ab1!").is_err());
assert!(meets_floor("Str0ng!Pass#99").is_ok());
assert!(meets_floor("correcthorsebatterystaple").is_ok());
assert_eq!(entropy_bits(""), 0.0);
}
}