zxcvbn 0.1.2 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +34 -2
- data/.travis.yml +4 -1
- data/CHANGELOG.md +15 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +45 -37
- data/bin/console +1 -1
- data/lib/zxcvbn.rb +14 -6
- data/lib/zxcvbn/adjacency_graphs.rb +46 -42
- data/lib/zxcvbn/feedback.rb +51 -53
- data/lib/zxcvbn/frequency_lists.rb +9 -7
- data/lib/zxcvbn/matching.rb +168 -191
- data/lib/zxcvbn/scoring.rb +91 -106
- data/lib/zxcvbn/time_estimates.rb +12 -14
- data/lib/zxcvbn/version.rb +1 -1
- data/zxcvbn.gemspec +2 -2
- metadata +9 -8
data/lib/zxcvbn/scoring.rb
CHANGED
@@ -6,44 +6,40 @@ module Zxcvbn
|
|
6
6
|
# this calculates the average over all keys.
|
7
7
|
def self.calc_average_degree(graph)
|
8
8
|
average = 0
|
9
|
-
graph.each do |
|
10
|
-
average += neighbors.count {|n| n }.to_f
|
9
|
+
graph.each do |_key, neighbors|
|
10
|
+
average += neighbors.count { |n| n }.to_f
|
11
11
|
end
|
12
12
|
average /= graph.keys.size.to_f
|
13
|
-
|
13
|
+
average
|
14
14
|
end
|
15
15
|
|
16
16
|
BRUTEFORCE_CARDINALITY = 10
|
17
17
|
|
18
|
-
MIN_GUESSES_BEFORE_GROWING_SEQUENCE =
|
18
|
+
MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10_000
|
19
19
|
|
20
20
|
MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10
|
21
21
|
|
22
22
|
MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50
|
23
23
|
|
24
|
-
def self.
|
24
|
+
def self.nck(n, k) # rubocop:disable Naming/MethodParameterName
|
25
25
|
# http://blog.plover.com/math/choose.html
|
26
|
-
if k > n
|
27
|
-
|
28
|
-
|
29
|
-
if k == 0
|
30
|
-
return 1.0
|
31
|
-
end
|
26
|
+
return 0.0 if k > n
|
27
|
+
return 1.0 if k == 0
|
28
|
+
|
32
29
|
r = 1.0
|
33
30
|
(1..k).each do |d|
|
34
31
|
r *= n
|
35
32
|
r /= d
|
36
33
|
n -= 1.0
|
37
34
|
end
|
38
|
-
|
35
|
+
r
|
39
36
|
end
|
40
37
|
|
41
|
-
def self.factorial(n)
|
38
|
+
def self.factorial(n) # rubocop:disable Naming/MethodParameterName
|
42
39
|
# unoptimized, called only on small n
|
43
|
-
if n < 2
|
44
|
-
|
45
|
-
|
46
|
-
return (2..n).reduce(&:*)
|
40
|
+
return 1 if n < 2
|
41
|
+
|
42
|
+
(2..n).reduce(&:*)
|
47
43
|
end
|
48
44
|
|
49
45
|
# ------------------------------------------------------------------------------
|
@@ -78,7 +74,7 @@ module Zxcvbn
|
|
78
74
|
# D^(l-1) approximates Sum(D^i for i in [1..l-1]
|
79
75
|
|
80
76
|
# ------------------------------------------------------------------------------
|
81
|
-
def self.most_guessable_match_sequence(password, matches, _exclude_additive
|
77
|
+
def self.most_guessable_match_sequence(password, matches, _exclude_additive: false)
|
82
78
|
n = password.length
|
83
79
|
# partition matches into sublists according to ending index j
|
84
80
|
matches_by_j = (0...n).map { [] }
|
@@ -88,7 +84,7 @@ module Zxcvbn
|
|
88
84
|
|
89
85
|
# small detail: for deterministic output, sort each sublist by i.
|
90
86
|
matches_by_j.each do |lst|
|
91
|
-
lst.sort_by!{|m| m["i"] }
|
87
|
+
lst.sort_by! { |m| m["i"] }
|
92
88
|
end
|
93
89
|
|
94
90
|
optimal = {
|
@@ -101,12 +97,12 @@ module Zxcvbn
|
|
101
97
|
# optimal.pi allows for fast (non-looping) updates to the minimization function.
|
102
98
|
"pi" => (0...n).map { {} },
|
103
99
|
# same structure as optimal.m -- holds the overall metric.
|
104
|
-
"g" => (0...n).map { {} }
|
100
|
+
"g" => (0...n).map { {} }
|
105
101
|
}
|
106
102
|
|
107
103
|
# helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
|
108
104
|
# than previously encountered sequences, updating state if so.
|
109
|
-
update =
|
105
|
+
update = lambda do |m, l|
|
110
106
|
k = m["j"]
|
111
107
|
pi = estimate_guesses(m, password)
|
112
108
|
if l > 1
|
@@ -117,30 +113,28 @@ module Zxcvbn
|
|
117
113
|
end
|
118
114
|
# calculate the minimization func
|
119
115
|
g = factorial(l) * pi
|
120
|
-
if !_exclude_additive
|
121
|
-
g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE ** (l - 1)
|
122
|
-
end
|
116
|
+
g += MIN_GUESSES_BEFORE_GROWING_SEQUENCE**(l - 1) if !_exclude_additive
|
123
117
|
# update state if new best.
|
124
118
|
# first see if any competing sequences covering this prefix, with l or fewer matches,
|
125
119
|
# fare better than this sequence. if so, skip it and return.
|
126
|
-
optimal["g"][k].
|
127
|
-
if competing_l > l
|
128
|
-
|
129
|
-
end
|
130
|
-
if competing_g <= g
|
131
|
-
return
|
132
|
-
end
|
120
|
+
optimal["g"][k].find do |competing_l, competing_g|
|
121
|
+
next if competing_l > l
|
122
|
+
return nil if competing_g <= g
|
133
123
|
end
|
134
124
|
# this sequence might be part of the final optimal sequence.
|
135
125
|
optimal["g"][k][l] = g
|
136
126
|
optimal["m"][k][l] = m
|
137
127
|
optimal["pi"][k][l] = pi
|
128
|
+
|
129
|
+
optimal["g"][k] = optimal["g"][k].sort.to_h
|
130
|
+
optimal["m"][k] = optimal["m"][k].sort.to_h
|
131
|
+
optimal["pi"][k] = optimal["pi"][k].sort.to_h
|
138
132
|
end
|
139
133
|
|
140
134
|
# helper: make bruteforce match objects spanning i to j, inclusive.
|
141
|
-
make_bruteforce_match =
|
135
|
+
make_bruteforce_match = lambda do |i, j|
|
142
136
|
return {
|
143
|
-
"pattern" =>
|
137
|
+
"pattern" => "bruteforce",
|
144
138
|
"token" => password[i..j],
|
145
139
|
"i" => i,
|
146
140
|
"j" => j
|
@@ -148,7 +142,7 @@ module Zxcvbn
|
|
148
142
|
end
|
149
143
|
|
150
144
|
# helper: evaluate bruteforce matches ending at k.
|
151
|
-
bruteforce_update =
|
145
|
+
bruteforce_update = lambda do |k|
|
152
146
|
# see if a single bruteforce match spanning the k-prefix is optimal.
|
153
147
|
m = make_bruteforce_match.call(0, k)
|
154
148
|
update.call(m, 1)
|
@@ -156,16 +150,14 @@ module Zxcvbn
|
|
156
150
|
# generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
|
157
151
|
# see if adding these new matches to any of the sequences in optimal[i-1]
|
158
152
|
# leads to new bests.
|
159
|
-
m = make_bruteforce_match.call(i, k)
|
160
|
-
optimal["m"][i-1].each do |l, last_m|
|
161
|
-
l = l.to_i
|
153
|
+
m = make_bruteforce_match.call(i, k)
|
154
|
+
optimal["m"][i - 1].each do |l, last_m|
|
162
155
|
# corner: an optimal sequence will never have two adjacent bruteforce matches.
|
163
156
|
# it is strictly better to have a single bruteforce match spanning the same region:
|
164
157
|
# same contribution to the guess product with a lower length.
|
165
158
|
# --> safe to skip those cases.
|
166
|
-
if last_m["pattern"] ==
|
167
|
-
|
168
|
-
end
|
159
|
+
next if last_m["pattern"] == "bruteforce"
|
160
|
+
|
169
161
|
# try adding m to this length-l sequence.
|
170
162
|
update.call(m, l + 1)
|
171
163
|
end
|
@@ -174,11 +166,11 @@ module Zxcvbn
|
|
174
166
|
|
175
167
|
# helper: step backwards through optimal.m starting at the end,
|
176
168
|
# constructing the final optimal match sequence.
|
177
|
-
unwind =
|
169
|
+
unwind = lambda do |n2|
|
178
170
|
optimal_match_sequence = []
|
179
|
-
k =
|
171
|
+
k = n2 - 1
|
180
172
|
# find the final best sequence length and score
|
181
|
-
l,
|
173
|
+
l, _g = (optimal["g"][k] || []).min_by { |_candidate_l, candidate_g| candidate_g || 0 }
|
182
174
|
while k >= 0
|
183
175
|
m = optimal["m"][k][l]
|
184
176
|
optimal_match_sequence.unshift(m)
|
@@ -191,7 +183,7 @@ module Zxcvbn
|
|
191
183
|
(0...n).each do |k|
|
192
184
|
matches_by_j[k].each do |m|
|
193
185
|
if m["i"] > 0
|
194
|
-
optimal["m"][m["i"] - 1].
|
186
|
+
optimal["m"][m["i"] - 1].each_key do |l|
|
195
187
|
update.call(m, l + 1)
|
196
188
|
end
|
197
189
|
else
|
@@ -205,14 +197,14 @@ module Zxcvbn
|
|
205
197
|
optimal_l = optimal_match_sequence.length
|
206
198
|
|
207
199
|
# corner: empty password
|
208
|
-
if password.
|
209
|
-
|
200
|
+
guesses = if password.empty?
|
201
|
+
1
|
210
202
|
else
|
211
|
-
|
203
|
+
optimal["g"][n - 1][optimal_l]
|
212
204
|
end
|
213
205
|
|
214
206
|
# final result object
|
215
|
-
|
207
|
+
{
|
216
208
|
"password" => password,
|
217
209
|
"guesses" => guesses,
|
218
210
|
"guesses_log10" => Math.log10(guesses),
|
@@ -227,6 +219,7 @@ module Zxcvbn
|
|
227
219
|
if match["guesses"]
|
228
220
|
return match["guesses"] # a match's guess estimate doesn't change. cache it.
|
229
221
|
end
|
222
|
+
|
230
223
|
min_guesses = 1
|
231
224
|
if match["token"].length < password.length
|
232
225
|
min_guesses = if match["token"].length == 1
|
@@ -242,22 +235,20 @@ module Zxcvbn
|
|
242
235
|
"repeat" => method(:repeat_guesses),
|
243
236
|
"sequence" => method(:sequence_guesses),
|
244
237
|
"regex" => method(:regex_guesses),
|
245
|
-
"date" => method(:date_guesses)
|
238
|
+
"date" => method(:date_guesses)
|
246
239
|
}
|
247
240
|
guesses = estimation_functions[match["pattern"]].call(match)
|
248
241
|
match["guesses"] = [guesses, min_guesses].max
|
249
242
|
match["guesses_log10"] = Math.log10(match["guesses"])
|
250
|
-
|
243
|
+
match["guesses"]
|
251
244
|
end
|
252
245
|
|
253
|
-
MAX_VALUE = 2
|
246
|
+
MAX_VALUE = 2**1024
|
254
247
|
|
255
248
|
def self.bruteforce_guesses(match)
|
256
|
-
guesses = BRUTEFORCE_CARDINALITY
|
249
|
+
guesses = BRUTEFORCE_CARDINALITY**match["token"].length
|
257
250
|
# trying to match JS behaviour here setting a MAX_VALUE to try to acheieve same values as JS library.
|
258
|
-
if guesses > MAX_VALUE
|
259
|
-
guesses = MAX_VALUE
|
260
|
-
end
|
251
|
+
guesses = MAX_VALUE if guesses > MAX_VALUE
|
261
252
|
|
262
253
|
# small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
|
263
254
|
# submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
|
@@ -271,29 +262,27 @@ module Zxcvbn
|
|
271
262
|
end
|
272
263
|
|
273
264
|
def self.repeat_guesses(match)
|
274
|
-
|
265
|
+
match["base_guesses"] * match["repeat_count"]
|
275
266
|
end
|
276
267
|
|
277
268
|
def self.sequence_guesses(match)
|
278
269
|
first_chr = match["token"][0]
|
279
270
|
# lower guesses for obvious starting points
|
280
|
-
if [
|
281
|
-
|
271
|
+
base_guesses = if ["a", "A", "z", "Z", "0", "1", "9"].include?(first_chr)
|
272
|
+
4
|
273
|
+
elsif first_chr.match?(/\d/)
|
274
|
+
10
|
282
275
|
else
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
# could give a higher base for uppercase,
|
287
|
-
# assigning 26 to both upper and lower sequences is more conservative.
|
288
|
-
base_guesses = 26
|
289
|
-
end
|
276
|
+
# could give a higher base for uppercase,
|
277
|
+
# assigning 26 to both upper and lower sequences is more conservative.
|
278
|
+
26
|
290
279
|
end
|
291
280
|
if !match["ascending"]
|
292
281
|
# need to try a descending sequence in addition to every ascending sequence ->
|
293
282
|
# 2x guesses
|
294
283
|
base_guesses *= 2
|
295
284
|
end
|
296
|
-
|
285
|
+
base_guesses * match["token"].length
|
297
286
|
end
|
298
287
|
|
299
288
|
MIN_YEAR_SPACE = 20
|
@@ -308,14 +297,14 @@ module Zxcvbn
|
|
308
297
|
"digits" => 10,
|
309
298
|
"symbols" => 33
|
310
299
|
}
|
311
|
-
if char_class_bases.
|
312
|
-
|
313
|
-
elsif match["regex_name"] ==
|
300
|
+
if char_class_bases.key? match["regex_name"]
|
301
|
+
char_class_bases[match["regex_name"]]**match["token"].length
|
302
|
+
elsif match["regex_name"] == "recent_year"
|
314
303
|
# conservative estimate of year space: num years from REFERENCE_YEAR.
|
315
304
|
# if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
|
316
305
|
year_space = (match["regex_match"][0].to_i - REFERENCE_YEAR).abs
|
317
|
-
|
318
|
-
|
306
|
+
[year_space, MIN_YEAR_SPACE].max
|
307
|
+
|
319
308
|
end
|
320
309
|
end
|
321
310
|
|
@@ -328,23 +317,23 @@ module Zxcvbn
|
|
328
317
|
# add factor of 4 for separator selection (one of ~4 choices)
|
329
318
|
guesses *= 4
|
330
319
|
end
|
331
|
-
|
320
|
+
guesses
|
332
321
|
end
|
333
322
|
|
334
|
-
KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"])
|
323
|
+
KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"]).freeze
|
335
324
|
# slightly different for keypad/mac keypad, but close enough
|
336
|
-
KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"])
|
325
|
+
KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"]).freeze
|
337
326
|
|
338
327
|
KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS["qwerty"].keys.size
|
339
328
|
KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS["keypad"].keys.size
|
340
329
|
|
341
330
|
def self.spatial_guesses(match)
|
342
|
-
if [
|
343
|
-
s = KEYBOARD_STARTING_POSITIONS
|
344
|
-
d = KEYBOARD_AVERAGE_DEGREE
|
331
|
+
if ["qwerty", "dvorak"].include?(match["graph"])
|
332
|
+
s = KEYBOARD_STARTING_POSITIONS
|
333
|
+
d = KEYBOARD_AVERAGE_DEGREE
|
345
334
|
else
|
346
|
-
s = KEYPAD_STARTING_POSITIONS
|
347
|
-
d = KEYPAD_AVERAGE_DEGREE
|
335
|
+
s = KEYPAD_STARTING_POSITIONS
|
336
|
+
d = KEYPAD_AVERAGE_DEGREE
|
348
337
|
end
|
349
338
|
guesses = 0.0
|
350
339
|
ll = match["token"].length
|
@@ -353,7 +342,7 @@ module Zxcvbn
|
|
353
342
|
(2..ll).each do |i|
|
354
343
|
possible_turns = [t, i - 1].min
|
355
344
|
(1..possible_turns).each do |j|
|
356
|
-
guesses +=
|
345
|
+
guesses += nck((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f**j.to_f)
|
357
346
|
end
|
358
347
|
end
|
359
348
|
# add extra guesses for shifted keys. (% instead of 5, A instead of a.)
|
@@ -366,12 +355,12 @@ module Zxcvbn
|
|
366
355
|
else
|
367
356
|
shifted_variations = 0
|
368
357
|
(1..[ss, uu].min).each do |i|
|
369
|
-
shifted_variations +=
|
358
|
+
shifted_variations += nck((ss + uu).to_f, i.to_f)
|
370
359
|
end
|
371
360
|
guesses *= shifted_variations
|
372
361
|
end
|
373
362
|
end
|
374
|
-
|
363
|
+
guesses
|
375
364
|
end
|
376
365
|
|
377
366
|
def self.dictionary_guesses(match)
|
@@ -379,49 +368,45 @@ module Zxcvbn
|
|
379
368
|
match["uppercase_variations"] = uppercase_variations(match)
|
380
369
|
match["l33t_variations"] = l33t_variations(match)
|
381
370
|
reversed_variations = match["reversed"] && 2 || 1
|
382
|
-
|
371
|
+
match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
|
383
372
|
end
|
384
373
|
|
385
|
-
START_UPPER = /^[A-Z][^A-Z]
|
386
|
-
END_UPPER = /^[^A-Z]+[A-Z]
|
387
|
-
ALL_UPPER = /^[^a-z]
|
388
|
-
ALL_LOWER = /^[^A-Z]
|
374
|
+
START_UPPER = /^[A-Z][^A-Z]+$/.freeze
|
375
|
+
END_UPPER = /^[^A-Z]+[A-Z]$/.freeze
|
376
|
+
ALL_UPPER = /^[^a-z]+$/.freeze
|
377
|
+
ALL_LOWER = /^[^A-Z]+$/.freeze
|
389
378
|
|
390
379
|
def self.uppercase_variations(match)
|
391
380
|
word = match["token"]
|
392
|
-
if word.match?(ALL_LOWER) || word.downcase
|
393
|
-
|
394
|
-
end
|
381
|
+
return 1 if word.match?(ALL_LOWER) || word.downcase == word
|
382
|
+
|
395
383
|
# a capitalized word is the most common capitalization scheme,
|
396
384
|
# so it only doubles the search space (uncapitalized + capitalized).
|
397
385
|
# allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
|
398
386
|
[START_UPPER, END_UPPER, ALL_UPPER].each do |regex|
|
399
|
-
if word.match?(regex)
|
400
|
-
return 2
|
401
|
-
end
|
387
|
+
return 2 if word.match?(regex)
|
402
388
|
end
|
403
389
|
# otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
|
404
390
|
# with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
|
405
391
|
# the number of ways to lowercase U+L letters with L lowercase letters or less.
|
406
|
-
uu = word.
|
407
|
-
ll = word.
|
408
|
-
variations = 0
|
392
|
+
uu = word.chars.count { |chr| chr.match?(/[A-Z]/) }
|
393
|
+
ll = word.chars.count { |chr| chr.match?(/[a-z]/) }
|
394
|
+
variations = 0
|
409
395
|
(1..[uu, ll].min).each do |i|
|
410
|
-
variations +=
|
396
|
+
variations += nck(uu + ll, i)
|
411
397
|
end
|
412
|
-
|
398
|
+
variations
|
413
399
|
end
|
414
400
|
|
415
401
|
def self.l33t_variations(match)
|
416
|
-
if !match["l33t"]
|
417
|
-
|
418
|
-
end
|
402
|
+
return 1 if !match["l33t"]
|
403
|
+
|
419
404
|
variations = 1
|
420
405
|
match["sub"].each do |subbed, unsubbed|
|
421
406
|
# lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
|
422
|
-
chrs = match["token"].downcase.
|
423
|
-
ss = chrs.count{|chr| chr == subbed }
|
424
|
-
uu = chrs.count{|chr| chr == unsubbed }
|
407
|
+
chrs = match["token"].downcase.chars
|
408
|
+
ss = chrs.count { |chr| chr == subbed }
|
409
|
+
uu = chrs.count { |chr| chr == unsubbed }
|
425
410
|
if ss == 0 || uu == 0
|
426
411
|
# for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
|
427
412
|
# treat that as doubling the space (attacker needs to try fully subbed chars in addition to
|
@@ -433,12 +418,12 @@ module Zxcvbn
|
|
433
418
|
p = [uu, ss].min
|
434
419
|
possibilities = 0
|
435
420
|
(1..p).each do |i|
|
436
|
-
possibilities +=
|
421
|
+
possibilities += nck(uu + ss, i)
|
437
422
|
end
|
438
423
|
variations *= possibilities
|
439
424
|
end
|
440
425
|
end
|
441
|
-
|
426
|
+
variations
|
442
427
|
end
|
443
428
|
end
|
444
429
|
end
|
@@ -9,36 +9,36 @@ module Zxcvbn
|
|
9
9
|
"offline_slow_hashing_1e4_per_second" => guesses / 1e4,
|
10
10
|
"offline_fast_hashing_1e10_per_second" => guesses / 1e10
|
11
11
|
}
|
12
|
-
crack_times_display = {}
|
12
|
+
crack_times_display = {}
|
13
13
|
crack_times_seconds.each do |scenario, seconds|
|
14
14
|
crack_times_display[scenario] = display_time(seconds)
|
15
15
|
end
|
16
16
|
|
17
|
-
|
17
|
+
{
|
18
18
|
"crack_times_seconds" => crack_times_seconds,
|
19
19
|
"crack_times_display" => crack_times_display,
|
20
|
-
"score" => guesses_to_score(guesses)
|
20
|
+
"score" => guesses_to_score(guesses)
|
21
21
|
}
|
22
22
|
end
|
23
23
|
|
24
24
|
def self.guesses_to_score(guesses)
|
25
|
-
delta = 5
|
25
|
+
delta = 5
|
26
26
|
if guesses < 1e3 + delta
|
27
27
|
# risky password: "too guessable"
|
28
|
-
|
28
|
+
0
|
29
29
|
elsif guesses < 1e6 + delta
|
30
30
|
# modest protection from throttled online attacks: "very guessable"
|
31
|
-
|
31
|
+
1
|
32
32
|
elsif guesses < 1e8 + delta
|
33
33
|
# modest protection from unthrottled online attacks: "somewhat guessable"
|
34
|
-
|
34
|
+
2
|
35
35
|
elsif guesses < 1e10 + delta
|
36
36
|
# modest protection from offline attacks: "safely unguessable"
|
37
37
|
# assuming a salted, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc
|
38
|
-
|
38
|
+
3
|
39
39
|
else
|
40
40
|
# strong protection from offline attacks under same scenario: "very unguessable"
|
41
|
-
|
41
|
+
4
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -50,7 +50,7 @@ module Zxcvbn
|
|
50
50
|
year = month * 12
|
51
51
|
century = year * 100
|
52
52
|
display_num, display_str = if seconds < 1
|
53
|
-
[nil,
|
53
|
+
[nil, "less than a second"]
|
54
54
|
elsif seconds < minute
|
55
55
|
base = seconds.round
|
56
56
|
[base, "#{base} second"]
|
@@ -70,11 +70,9 @@ module Zxcvbn
|
|
70
70
|
base = (seconds / year).round
|
71
71
|
[base, "#{base} year"]
|
72
72
|
else
|
73
|
-
[nil,
|
74
|
-
end
|
75
|
-
if display_num && display_num != 1
|
76
|
-
display_str += 's'
|
73
|
+
[nil, "centuries"]
|
77
74
|
end
|
75
|
+
display_str += "s" if display_num && display_num != 1
|
78
76
|
display_str
|
79
77
|
end
|
80
78
|
end
|