zxcvbn_cli/
lib.rs

1//! A simple CLI tool to check password strength using zxcvbn.
2//!
3//! See [README.md](https://github.com/u32i64/zxcvbn-cli#readme) for more information.
4#![deny(missing_docs)]
5
6use colored::Colorize;
7use zxcvbn::{Entropy, Match};
8
9macro_rules! info {
10    ($name:expr) => {
11        println!("{}", $name.bright_blue().bold());
12    };
13    ($name:expr, $value:expr) => {
14        println!("{} {}", $name.bright_blue().bold(), $value);
15    };
16}
17
18/// Runs the main logic of the app.
19pub fn run(password: &str, hide_password: bool) -> Result<(), String> {
20    if password.is_empty() {
21        return Err("empty password".into());
22    }
23
24    let entropy = zxcvbn::zxcvbn(password, &[]).unwrap();
25
26    print!("{} ", "➜".magenta());
27    if hide_password {
28        print!("{}", "<hidden>".magenta());
29    } else {
30        print_password_tokenized(&entropy);
31    }
32    println!();
33
34    main_information(&entropy);
35    guesses(&entropy);
36    crack_time(&entropy);
37
38    if !hide_password {
39        info!("sequence");
40        sequence(entropy.sequence(), 0);
41    }
42
43    println!();
44    println!(
45        "{}",
46        format!(
47            "zxcvbn done in {} ms",
48            entropy.calculation_time().as_millis()
49        )
50        .bright_black()
51    );
52
53    Ok(())
54}
55
56fn print_password_tokenized(entropy: &Entropy) {
57    for (i, token) in entropy.sequence().iter().map(|m| &m.token).enumerate() {
58        print!(
59            "{}",
60            if (i + 1) % 2 == 0 {
61                token.on_bright_black()
62            } else {
63                token.normal()
64            }
65        );
66    }
67}
68
69fn main_information(entropy: &Entropy) {
70    info!(
71        "score",
72        format!(
73            "{}{}",
74            match entropy.score() {
75                0 => "0".bright_red(),
76                1 => "1".red(),
77                2 => "2".yellow(),
78                3 => "3".green(),
79                4 => "4".bright_green(),
80                _ => unreachable!(),
81            },
82            " / 4"
83        )
84        .bold()
85    );
86
87    if let Some(feedback) = entropy.feedback().clone() {
88        if let Some(warning) = feedback.warning() {
89            println!("{} {}", "warning".bright_yellow().bold(), warning);
90        }
91
92        if !feedback.suggestions().is_empty() {
93            println!("{}", "suggestions".bright_green().bold());
94
95            let i_last = feedback.suggestions().len() - 1;
96            for (i, suggestion) in feedback.suggestions().iter().enumerate() {
97                println!(
98                    "{} {}",
99                    if i < i_last { "  ├" } else { "  └" }.bright_green().bold(),
100                    suggestion
101                );
102            }
103        }
104    }
105}
106
107fn guesses(entropy: &Entropy) {
108    info!(
109        "guesses",
110        "1e".to_string() + &entropy.guesses_log10().to_string()
111    );
112}
113
114macro_rules! fmt_crack_time {
115    ($entropy:expr, $name:ident, $desc:expr) => {
116        format!(
117            "{} {}",
118            $entropy.crack_times().$name().to_string(),
119            $desc.bright_black(),
120        )
121    };
122}
123
124fn crack_time(entropy: &Entropy) {
125    info!("crack time");
126    info!(
127        "  ├ 10²/hour",
128        fmt_crack_time!(entropy, online_throttling_100_per_hour, "online, throttled")
129    );
130    info!(
131        "  ├ 10  /sec",
132        fmt_crack_time!(
133            entropy,
134            online_no_throttling_10_per_second,
135            "online, not throttled"
136        )
137    );
138    info!(
139        "  ├ 10⁴ /sec",
140        fmt_crack_time!(
141            entropy,
142            offline_slow_hashing_1e4_per_second,
143            "offline, slow hash"
144        )
145    );
146    info!(
147        "  └ 10¹⁰/sec",
148        fmt_crack_time!(
149            entropy,
150            offline_fast_hashing_1e10_per_second,
151            "offline, fast hash"
152        )
153    );
154}
155
156macro_rules! pattern_info {
157    ($name:expr, $value:expr, $indent:expr) => {
158        println!(
159            "  {}{} {} {}",
160            " ".repeat($indent),
161            "├".bright_blue(),
162            $name.bright_blue(),
163            $value
164        );
165    };
166    (last $name:expr, $value:expr, $indent:expr) => {
167        println!(
168            "  {}{} {} {}",
169            " ".repeat($indent),
170            "└".bright_blue(),
171            $name.bright_blue(),
172            $value
173        );
174    };
175    (type $name:expr, $indent:expr) => {
176        println!(
177            "  {}{} type {}",
178            " ".repeat($indent),
179            "├".bright_blue(),
180            $name.blue()
181        );
182    };
183    (type last $name:expr, $indent:expr) => {
184        println!(
185            "  {}{} type {}",
186            " ".repeat($indent),
187            "└".bright_blue(),
188            $name.blue()
189        )
190    };
191}
192
193fn sequence(seq: &[Match], indent: usize) {
194    use zxcvbn::matching::patterns::MatchPattern::*;
195
196    for (i, part) in seq.iter().enumerate() {
197        println!(
198            "  {}{}",
199            " ".repeat(indent),
200            if (i + 1) % 2 == 0 {
201                part.token.underline().on_bright_black()
202            } else {
203                part.token.underline()
204            }
205        );
206
207        match &part.pattern {
208            Dictionary(pattern) => {
209                pattern_info!(type "dictionary", indent);
210                pattern_info!("word", pattern.matched_word, indent);
211                pattern_info!("rank", pattern.rank, indent);
212                pattern_info!(
213                    "dictionary",
214                    format!("{:?}", pattern.dictionary_name),
215                    indent
216                );
217                pattern_info!("reversed?", pattern.reversed, indent);
218                pattern_info!("l33t?", pattern.l33t, indent);
219                if let Some(substitutions) = &pattern.sub_display {
220                    pattern_info!("substitutions", substitutions, indent);
221                }
222                pattern_info!("uppercase variations", pattern.uppercase_variations, indent);
223                pattern_info!("l33t variations", pattern.l33t_variations, indent);
224                pattern_info!(last "base guesses", pattern.base_guesses, indent);
225            }
226            Spatial(pattern) => {
227                pattern_info!(type "spatial", indent);
228                pattern_info!("graph", pattern.graph, indent);
229                pattern_info!("turns", pattern.turns, indent);
230                pattern_info!(last "shifts", pattern.shifted_count, indent);
231            }
232            Repeat(pattern) => {
233                pattern_info!(type "repeat", indent);
234                pattern_info!("base token", pattern.base_token, indent);
235                pattern_info!("base matches", "", indent);
236                sequence(&pattern.base_matches, indent + 2);
237                pattern_info!("base guesses", pattern.base_guesses, indent);
238                pattern_info!(last "repeat count", pattern.repeat_count, indent);
239            }
240            Sequence(pattern) => {
241                pattern_info!(type "sequence", indent);
242                pattern_info!("name", pattern.sequence_name, indent);
243                pattern_info!("space", pattern.sequence_space, indent);
244                pattern_info!(last "ascending?", pattern.ascending, indent);
245            }
246            Regex(pattern) => {
247                pattern_info!(type "regex", indent);
248                pattern_info!("name", pattern.regex_name, indent);
249                pattern_info!(last "match", format!("{:?}", pattern.regex_match), indent);
250            }
251            Date(pattern) => {
252                pattern_info!(type "date", indent);
253                pattern_info!("separator", pattern.separator, indent);
254                pattern_info!("year", pattern.year, indent);
255                pattern_info!("month", pattern.month, indent);
256                pattern_info!(last "day", pattern.day, indent);
257            }
258            BruteForce => pattern_info!(type last "bruteforce", indent),
259        }
260    }
261}