use super::funejson::Value; use super::auth_password_policy_data::COMMON_PASSWORDS; /// Words shorter than this in a name or email are not refused inside a /// password: "al" or "jo" would refuse half of all passwords. const MIN_PERSONAL: usize = 3; /// Pieces between ASCII punctuation and spaces, lower-cased (ASCII only, as /// in the other languages), 3 or more characters. fn words(text: &str) -> Vec { let mut out = Vec::new(); let mut current = String::new(); for ch in text.chars() { if ch.is_ascii() && !ch.is_ascii_alphanumeric() { if current.chars().count() >= MIN_PERSONAL { out.push(current.to_ascii_lowercase()); } current.clear(); } else { current.push(ch); } } if current.chars().count() >= MIN_PERSONAL { out.push(current.to_ascii_lowercase()); } out } fn failure(code: &str, message: String) -> PasswordFailure { PasswordFailure { code: code.to_string(), message } } /// Every rule of the policy the password breaks, in a fixed order, each with /// a sentence to show beside the field. The same function runs in the /// browser and on the server. /// /// # Panics /// Panics on a policy whose numbers make no sense. pub fn check_password(password: &str, email: Option<&str>, name: Option<&str>, policy: &PasswordPolicy) -> PasswordCheck { if policy.min_length < 1 { panic!("policy minLength must be a whole number of at least 1"); } if policy.max_length < policy.min_length { panic!("policy maxLength must be a whole number no smaller than minLength"); } if !(0..=4).contains(&policy.min_character_classes) { panic!("policy minCharacterClasses must be a whole number from 0 to 4"); } let mut failures = Vec::new(); let length = password.chars().count() as i64; if length < policy.min_length { failures.push(failure("too_short", format!("Use at least {} characters.", policy.min_length))); } if length > policy.max_length { failures.push(failure("too_long", format!("Use no more than {} characters.", policy.max_length))); } if policy.min_character_classes > 0 { let (mut lower, mut upper, mut digit, mut other) = (0, 0, 0, 0); for ch in password.chars() { match ch { 'a'..='z' => lower = 1, 'A'..='Z' => upper = 1, '0'..='9' => digit = 1, _ => other = 1, } } if lower + upper + digit + other < policy.min_character_classes { failures.push(failure( "too_few_character_classes", format!( "Use at least {} of these: lower-case letters, capital letters, digits, symbols.", policy.min_character_classes ), )); } } let folded = password.to_ascii_lowercase(); if policy.block_common && COMMON_PASSWORDS.iter().any(|row| row.password == folded) { failures.push(failure( "too_common", "This password is too common. Choose something harder to guess.".to_string(), )); } if policy.block_personal { if let Some(email) = email { let local = match email.rfind('@') { Some(at) => &email[..at], None => email, }; let mut candidates = words(local); if local.chars().count() >= MIN_PERSONAL { candidates.push(local.to_ascii_lowercase()); } if candidates.iter().any(|word| folded.contains(word.as_str())) { failures.push(failure( "contains_email", "Do not include your email address in your password.".to_string(), )); } } if let Some(name) = name { if words(name).iter().any(|word| folded.contains(word.as_str())) { failures.push(failure("contains_name", "Do not include your name in your password.".to_string())); } } } PasswordCheck { valid: failures.is_empty(), failures } } pub fn password_check_to_value(check: &PasswordCheck) -> Value { Value::obj(vec![ ("valid", Value::Bool(check.valid)), ( "failures", Value::Arr( check .failures .iter() .map(|f| Value::obj(vec![("code", Value::str(&f.code)), ("message", Value::str(&f.message))])) .collect(), ), ), ]) } /// A PasswordPolicy from vector JSON, refusing a fractional or missing /// number with the words the policy check uses. pub fn password_policy_from_value(value: &Value) -> PasswordPolicy { let int = |key: &str, message: &str| match value.get(key) { Value::Int(i) => *i, _ => panic!("{}", message), }; PasswordPolicy { name: value.get("name").as_str().to_string(), min_length: int("minLength", "policy minLength must be a whole number of at least 1"), max_length: int("maxLength", "policy maxLength must be a whole number no smaller than minLength"), min_character_classes: int("minCharacterClasses", "policy minCharacterClasses must be a whole number from 0 to 4"), block_common: value.get("blockCommon").as_bool(), block_personal: value.get("blockPersonal").as_bool(), } } pub fn fune_vector(args: &[Value]) -> Value { let text = |v: &Value| match v { Value::Str(s) => Some(s.clone()), _ => None, }; let password = match &args[0] { Value::Str(s) => s.clone(), _ => panic!("password must be a string"), }; let email = text(&args[1]); let name = text(&args[2]); let policy = password_policy_from_value(&args[3]); password_check_to_value(&check_password(&password, email.as_deref(), name.as_deref(), &policy)) }