From a27723ea5ec169b9b395fc14144a28e5407f0bce Mon Sep 17 00:00:00 2001 From: Randolf J <19544887-randolf@users.noreply.gitlab.com> Date: Sat, 11 May 2024 13:09:09 -0700 Subject: [PATCH] Use enum for score --- src/feedback.rs | 6 ++--- src/lib.rs | 27 +++++++++++---------- src/scoring.rs | 56 ++++++++++++++++++++++++++++++++++++++++++- src/time_estimates.rs | 16 +++++++------ 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/feedback.rs b/src/feedback.rs index 26603a1..03e68eb 100644 --- a/src/feedback.rs +++ b/src/feedback.rs @@ -1,9 +1,9 @@ //! Contains structs and methods related to generating feedback strings //! for providing help for the user to generate stronger passwords. -use crate::frequency_lists::DictionaryType; use crate::matching::patterns::*; use crate::matching::Match; +use crate::{frequency_lists::DictionaryType, scoring::Score}; use std::fmt; /// A warning explains what's wrong with the password. @@ -153,7 +153,7 @@ impl Feedback { } } -pub(crate) fn get_feedback(score: u8, sequence: &[Match]) -> Option { +pub(crate) fn get_feedback(score: Score, sequence: &[Match]) -> Option { if sequence.is_empty() { // default feedback return Some(Feedback { @@ -164,7 +164,7 @@ pub(crate) fn get_feedback(score: u8, sequence: &[Match]) -> Option { ], }); } - if score >= 3 { + if score >= Score::Three { return None; } diff --git a/src/lib.rs b/src/lib.rs index 20121e7..d7cfa54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ use std::time::Duration; #[macro_use] extern crate quickcheck; +use scoring::Score; use time_estimates::CrackTimes; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::wasm_bindgen; @@ -77,7 +78,7 @@ pub struct Entropy { crack_times: time_estimates::CrackTimes, /// Overall strength score from 0-4. /// Any score less than 3 should be considered too weak. - score: u8, + score: Score, /// Verbal feedback to help choose better passwords. Set when `score` <= 2. feedback: Option, /// The list of patterns the guess calculation was based on @@ -104,7 +105,7 @@ impl Entropy { /// Overall strength score from 0-4. /// Any score less than 3 should be considered too weak. - pub fn score(&self) -> u8 { + pub fn score(&self) -> Score { self.score } @@ -133,8 +134,8 @@ pub fn zxcvbn(password: &str, user_inputs: &[&str]) -> Entropy { guesses: 0, guesses_log10: f64::NEG_INFINITY, crack_times: CrackTimes::new(0), - score: 0, - feedback: feedback::get_feedback(0, &[]), + score: Score::Zero, + feedback: feedback::get_feedback(Score::Zero, &[]), sequence: Vec::default(), calc_time: Duration::from_secs(0), }; @@ -198,7 +199,7 @@ mod tests { let password = "r0sebudmaelstrom11/20/91aaaa"; let entropy = zxcvbn(password, &[]); assert_eq!(entropy.guesses_log10 as u16, 14); - assert_eq!(entropy.score, 4); + assert_eq!(entropy.score, Score::Four); assert!(!entropy.sequence.is_empty()); assert!(entropy.feedback.is_none()); assert!(entropy.calc_time.as_nanos() > 0); @@ -209,7 +210,7 @@ mod tests { fn test_zxcvbn_empty() { let password = ""; let entropy = zxcvbn(password, &[]); - assert_eq!(entropy.score, 0); + assert_eq!(entropy.score, Score::Zero); assert_eq!(entropy.guesses, 0); assert_eq!(entropy.guesses_log10, f64::NEG_INFINITY); assert_eq!(entropy.crack_times, CrackTimes::new(0)); @@ -221,7 +222,7 @@ mod tests { fn test_zxcvbn_unicode() { let password = "𐰊𐰂𐰄𐰀𐰁"; let entropy = zxcvbn(password, &[]); - assert_eq!(entropy.score, 1); + assert_eq!(entropy.score, Score::One); } #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -229,7 +230,7 @@ mod tests { fn test_zxcvbn_unicode_2() { let password = "r0sebudmaelstrom丂/20/91aaaa"; let entropy = zxcvbn(password, &[]); - assert_eq!(entropy.score, 4); + assert_eq!(entropy.score, Score::Four); } #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -237,7 +238,7 @@ mod tests { fn test_issue_13() { let password = "Imaginative-Say-Shoulder-Dish-0"; let entropy = zxcvbn(password, &[]); - assert_eq!(entropy.score, 4); + assert_eq!(entropy.score, Score::Four); } #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -247,7 +248,7 @@ mod tests { let entropy = zxcvbn(password, &[]); assert_eq!(entropy.guesses, 372_010_000); assert!((entropy.guesses_log10 - 8.57055461430783).abs() < f64::EPSILON); - assert_eq!(entropy.score, 3); + assert_eq!(entropy.score, Score::Three); } #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -257,7 +258,7 @@ mod tests { let entropy = zxcvbn(password, &[]); assert_eq!(entropy.guesses, 1_010_000); assert!((entropy.guesses_log10 - 6.004321373782642).abs() < f64::EPSILON); - assert_eq!(entropy.score, 2); + assert_eq!(entropy.score, Score::Two); } #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -266,7 +267,7 @@ mod tests { let password = "!QASW@#EDFR$%TGHY^&UJKI*(OL"; let entropy = zxcvbn(password, &[]); assert_eq!(entropy.guesses, u64::max_value()); - assert_eq!(entropy.score, 4); + assert_eq!(entropy.score, Score::Four); } #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -275,6 +276,6 @@ mod tests { let password = "08märz2010"; let entropy = zxcvbn(password, &[]); assert_eq!(entropy.guesses, 100010000); - assert_eq!(entropy.score, 3); + assert_eq!(entropy.score, Score::Three); } } diff --git a/src/scoring.rs b/src/scoring.rs index ca76f7b..1107e16 100644 --- a/src/scoring.rs +++ b/src/scoring.rs @@ -1,7 +1,61 @@ use crate::matching::patterns::*; use crate::matching::Match; -use std::cmp; use std::collections::HashMap; +use std::{cmp, fmt::Display}; + +#[derive(Debug, Clone, Copy, Hash)] +#[non_exhaustive] +pub enum Score { + /// Can be cracked with 10^3 guesses or less. + Zero = 0, + /// Can be cracked with 10^6 guesses or less. + One, + /// Can be cracked with 10^8 guesses or less. + Two, + /// Can be cracked with 10^10 guesses or less. + Three, + /// Requires more than 10^10 guesses to crack. + Four, +} + +impl From for i8 { + fn from(score: Score) -> i8 { + score as i8 + } +} + +impl + Copy> PartialEq for Score { + fn eq(&self, other: &T) -> bool { + i8::from(*self) == (*other).into() + } +} + +impl Eq for Score {} + +impl + Copy> PartialOrd for Score { + fn partial_cmp(&self, other: &T) -> Option { + i8::from(*self).partial_cmp(&(*other).into()) + } +} + +impl Ord for Score { + fn cmp(&self, other: &Self) -> cmp::Ordering { + i8::from(*self).cmp(&i8::from(*other)) + } +} + +impl Display for Score { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", i8::from(*self)) + } +} + +#[cfg(feature = "ser")] +impl serde::Serialize for Score { + fn serialize(&self, serializer: S) -> Result { + i8::from(self).serialize(serializer) + } +} #[derive(Debug, Clone)] pub struct GuessCalculation { diff --git a/src/time_estimates.rs b/src/time_estimates.rs index 0aab65e..3530aad 100644 --- a/src/time_estimates.rs +++ b/src/time_estimates.rs @@ -22,6 +22,8 @@ use std::fmt; +use crate::scoring::Score; + /// Back-of-the-envelope crack time estimations, in seconds, based on a few scenarios. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] #[cfg_attr(feature = "ser", derive(Serialize))] @@ -129,21 +131,21 @@ impl From for std::time::Duration { } } -pub(crate) fn estimate_attack_times(guesses: u64) -> (CrackTimes, u8) { +pub(crate) fn estimate_attack_times(guesses: u64) -> (CrackTimes, Score) { (CrackTimes::new(guesses), calculate_score(guesses)) } -fn calculate_score(guesses: u64) -> u8 { +fn calculate_score(guesses: u64) -> Score { const DELTA: u64 = 5; if guesses < 1_000 + DELTA { - 0 + Score::Zero } else if guesses < 1_000_000 + DELTA { - 1 + Score::One } else if guesses < 100_000_000 + DELTA { - 2 + Score::Two } else if guesses < 10_000_000_000 + DELTA { - 3 + Score::Three } else { - 4 + Score::Four } }