1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
<?php class ITSEC_Zxcvbn_Results { /** * @var string Password */ public $password;
/** * @var int Estimated guesses needed to crack password */ public $guesses;
/** * @var int Order of magnitude of result.guesses */ public $guesses_log10;
/** * @var object dictionary of back-of-the-envelope crack time estimations, in seconds, based on a few scenarios { * @type int $offline_fast_hashing_1e10_per_second offline attack with user-unique salting but a fast hash function like SHA-1, SHA-256 or MD5. A wide range of reasonable numbers anywhere from one billion - one trillion guesses per second, depending on number of cores and machines. ballparking at 10B/sec. * @type int $offline_slow_hashing_1e4_per_second offline attack. assumes multiple attackers, proper user-unique salting, and a slow hash function w/ moderate work factor, such as bcrypt, scrypt, PBKDF2. * @type int $online_no_throttling_10_per_second online attack on a service that doesn't ratelimit, or where an attacker has outsmarted ratelimiting * @type int $online_throttling_100_per_hour online attack on a service that ratelimits password auth attempts. * } */ public $crack_times_seconds = array();
/** * @var object same keys as result.crack_times_seconds, with friendlier display string values: "less than a second", "3 hours", "centuries", etc. { * @type int $offline_fast_hashing_1e10_per_second offline attack with user-unique salting but a fast hash function like SHA-1, SHA-256 or MD5. A wide range of reasonable numbers anywhere from one billion - one trillion guesses per second, depending on number of cores and machines. ballparking at 10B/sec. * @type int $offline_slow_hashing_1e4_per_second offline attack. assumes multiple attackers, proper user-unique salting, and a slow hash function w/ moderate work factor, such as bcrypt, scrypt, PBKDF2. * @type int $online_no_throttling_10_per_second online attack on a service that doesn't ratelimit, or where an attacker has outsmarted ratelimiting * @type int $online_throttling_100_per_hour online attack on a service that ratelimits password auth attempts. * } */ public $crack_times_display = array();
/** * @var int from 0-4 (useful for implementing a strength bar) * 0 # too guessable: risky password. (guesses < 10^3) * 1 # very guessable: protection from throttled online attacks. (guesses < 10^6) * 2 # somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) * 3 # safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) * 4 # very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) */ public $score;
/** * @var object verbal feedback to help choose better passwords. set when score <= 2. { * @type string $warning explains what's wrong, eg. 'this is a top-10 common password'. not always set -- sometimes an empty string * @type array $suggestions a possibly-empty list of suggestions to help choose a less guessable password. eg. 'Add another word or two' * } */ public $feedback;
/** * @var array the list of patterns that zxcvbn based the guess calculation on. */ public $sequence;
/** * @var int how long it took zxcvbn to calculate an answer, in milliseconds */ public $calc_time;
public function __construct( $password, $guesses, $optimal_match_sequence ) { $this->password = $password; $this->guesses = $guesses; $this->guesses_log10 = log10( $guesses ); $this->sequence = $optimal_match_sequence;
$this->estimate_attack_times(); $this->calc_score(); $this->feedback(); }
protected function estimate_attack_times() { $this->crack_times_seconds = array( 'online_throttling_100_per_hour' => $this->guesses / ( 100 / 3600 ), 'online_no_throttling_10_per_second' => $this->guesses / 10, 'offline_slow_hashing_1e4_per_second' => $this->guesses / 1e4, 'offline_fast_hashing_1e10_per_second' => $this->guesses / 1e10 );
foreach ( $this->crack_times_seconds as $key => $seconds ) { $this->crack_times_display[ $key ] = $this->display_time( $seconds ); } }
protected function calc_score() { $delta = 5; if ( $this->guesses < 1e3 + $delta ) { $this->score = 0; } elseif ( $this->guesses < 1e6 + $delta ) { $this->score = 1; } elseif ( $this->guesses < 1e8 + $delta ) { $this->score = 2; } elseif ( $this->guesses < 1e10 + $delta ) { $this->score = 3; } else { $this->score = 4; } }
protected function display_time( $seconds ) { $minute = 60; $hour = $minute * 60; $day = $hour * 24; $month = $day * 31; $year = $month * 12; $century = $year * 100; if ( $seconds < 1 ) { return 'less than a second'; } elseif( $seconds < $minute ) { return sprintf( '%d second%s', $seconds, ( $seconds > 1 )? 's':'' ); } elseif( $seconds < $hour ) { $base = round( $seconds / $minute ); return sprintf( '%d minute%s', $base, ( $base > 1 )? 's':'' ); } elseif( $seconds < $day ) { $base = round( $seconds / $hour ); return sprintf( '%d hour%s', $base, ( $base > 1 )? 's':'' ); } elseif( $seconds < $month ) { $base = round( $seconds / $day ); return sprintf( '%d day%s', $base, ( $base > 1 )? 's':'' ); } elseif( $seconds < $year ) { $base = round( $seconds / $month ); return sprintf( '%d month%s', $base, ( $base > 1 )? 's':'' ); } elseif( $seconds < $century ) { $base = round( $seconds / $year ); return sprintf( '%d year%s', $base, ( $base > 1 )? 's':'' ); } else { return 'centuries'; } }
protected function feedback() { $this->feedback = new stdClass(); $this->feedback->warning = ''; $this->feedback->suggestions = array();
if ( 0 === count( $this->sequence ) ) { $this->feedback->suggestions = array( 'Use a few words, avoid common phrases', 'No need for symbols, digits, or uppercase letters' ); return; }
if ( $this->score > 2 ) { return; }
// tie feedback to the longest match for longer sequences $longest_match = $this->sequence[0]; foreach ( $this->sequence as $m ) { if ( strlen( $m->token ) > strlen( $longest_match->token ) ) { $longest_match = $m; } }
$is_sole_match = count( $this->sequence ) == 1;
if ( is_object( $longest_match ) && is_callable( array( $longest_match, 'get_feedback' ) ) ) { $this->feedback = $longest_match->get_feedback( $is_sole_match ); } } }
|