zxcvbn 0.1.1 → 0.1.6
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 +1 -1
- data/CHANGELOG.md +11 -1
- data/Gemfile +9 -4
- data/Gemfile.lock +26 -1
- data/README.md +45 -37
- data/bin/console +5 -2
- data/lib/zxcvbn.rb +17 -9
- data/lib/zxcvbn/adjacency_graphs.rb +50 -46
- data/lib/zxcvbn/feedback.rb +68 -70
- data/lib/zxcvbn/frequency_lists.rb +9 -7
- data/lib/zxcvbn/matching.rb +232 -249
- data/lib/zxcvbn/scoring.rb +153 -167
- data/lib/zxcvbn/time_estimates.rb +18 -20
- 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 }
|
9
|
+
graph.each do |_key, neighbors|
|
10
|
+
average += neighbors.count { |n| n }.to_f
|
11
11
|
end
|
12
|
-
average /= graph.keys.size
|
13
|
-
|
12
|
+
average /= graph.keys.size.to_f
|
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
|
-
|
30
|
-
return 1
|
31
|
-
end
|
32
|
-
r = 1
|
26
|
+
return 0.0 if k > n
|
27
|
+
return 1.0 if k == 0
|
28
|
+
|
29
|
+
r = 1.0
|
33
30
|
(1..k).each do |d|
|
34
31
|
r *= n
|
35
32
|
r /= d
|
36
|
-
n -= 1
|
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..10).reduce(&:*)
|
40
|
+
return 1 if n < 2
|
41
|
+
|
42
|
+
(2..n).reduce(&:*)
|
47
43
|
end
|
48
44
|
|
49
45
|
# ------------------------------------------------------------------------------
|
@@ -78,17 +74,17 @@ 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 { [] }
|
85
81
|
matches.each do |m|
|
86
|
-
matches_by_j[m[
|
82
|
+
matches_by_j[m["j"]] << m
|
87
83
|
end
|
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[
|
87
|
+
lst.sort_by! { |m| m["i"] }
|
92
88
|
end
|
93
89
|
|
94
90
|
optimal = {
|
@@ -96,59 +92,57 @@ module Zxcvbn
|
|
96
92
|
# password prefix up to k, inclusive.
|
97
93
|
# if there is no length-l sequence that scores better (fewer guesses) than
|
98
94
|
# a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
|
99
|
-
m
|
95
|
+
"m" => (0...n).map { {} },
|
100
96
|
# same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
|
101
97
|
# optimal.pi allows for fast (non-looping) updates to the minimization function.
|
102
|
-
pi
|
98
|
+
"pi" => (0...n).map { {} },
|
103
99
|
# same structure as optimal.m -- holds the overall metric.
|
104
|
-
g
|
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 =
|
110
|
-
k = m[
|
105
|
+
update = lambda do |m, l|
|
106
|
+
k = m["j"]
|
111
107
|
pi = estimate_guesses(m, password)
|
112
108
|
if l > 1
|
113
109
|
# we're considering a length-l sequence ending with match m:
|
114
110
|
# obtain the product term in the minimization function by multiplying m's guesses
|
115
111
|
# by the product of the length-(l-1) sequence ending just before m, at m.i - 1.
|
116
|
-
pi *= optimal[
|
112
|
+
pi *= optimal["pi"][m["i"] - 1][l - 1]
|
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[
|
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
|
-
optimal[
|
136
|
-
optimal[
|
137
|
-
optimal[
|
125
|
+
optimal["g"][k][l] = g
|
126
|
+
optimal["m"][k][l] = m
|
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
|
144
|
-
token
|
145
|
-
i
|
146
|
-
j
|
137
|
+
"pattern" => "bruteforce",
|
138
|
+
"token" => password[i..j],
|
139
|
+
"i" => i,
|
140
|
+
"j" => j
|
147
141
|
}
|
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[
|
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[
|
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,15 +166,15 @@ 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
|
-
m = optimal[
|
175
|
+
m = optimal["m"][k][l]
|
184
176
|
optimal_match_sequence.unshift(m)
|
185
|
-
k = m[
|
177
|
+
k = m["i"] - 1
|
186
178
|
l -= 1
|
187
179
|
end
|
188
180
|
return optimal_match_sequence
|
@@ -190,8 +182,8 @@ module Zxcvbn
|
|
190
182
|
|
191
183
|
(0...n).each do |k|
|
192
184
|
matches_by_j[k].each do |m|
|
193
|
-
if m[
|
194
|
-
optimal[
|
185
|
+
if m["i"] > 0
|
186
|
+
optimal["m"][m["i"] - 1].each_key do |l|
|
195
187
|
update.call(m, l + 1)
|
196
188
|
end
|
197
189
|
else
|
@@ -205,18 +197,18 @@ 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
|
-
|
216
|
-
password
|
217
|
-
guesses
|
218
|
-
guesses_log10
|
219
|
-
sequence
|
207
|
+
{
|
208
|
+
"password" => password,
|
209
|
+
"guesses" => guesses,
|
210
|
+
"guesses_log10" => Math.log10(guesses),
|
211
|
+
"sequence" => optimal_match_sequence
|
220
212
|
}
|
221
213
|
end
|
222
214
|
|
@@ -224,76 +216,73 @@ module Zxcvbn
|
|
224
216
|
# guess estimation -- one function per match pattern ---------------------------
|
225
217
|
# ------------------------------------------------------------------------------
|
226
218
|
def self.estimate_guesses(match, password)
|
227
|
-
if match[
|
228
|
-
return match[
|
219
|
+
if match["guesses"]
|
220
|
+
return match["guesses"] # a match's guess estimate doesn't change. cache it.
|
229
221
|
end
|
222
|
+
|
230
223
|
min_guesses = 1
|
231
|
-
if match[
|
232
|
-
min_guesses = if match[
|
224
|
+
if match["token"].length < password.length
|
225
|
+
min_guesses = if match["token"].length == 1
|
233
226
|
MIN_SUBMATCH_GUESSES_SINGLE_CHAR
|
234
227
|
else
|
235
228
|
MIN_SUBMATCH_GUESSES_MULTI_CHAR
|
236
229
|
end
|
237
230
|
end
|
238
231
|
estimation_functions = {
|
239
|
-
bruteforce
|
240
|
-
dictionary
|
241
|
-
spatial
|
242
|
-
repeat
|
243
|
-
sequence
|
244
|
-
regex
|
245
|
-
date
|
232
|
+
"bruteforce" => method(:bruteforce_guesses),
|
233
|
+
"dictionary" => method(:dictionary_guesses),
|
234
|
+
"spatial" => method(:spatial_guesses),
|
235
|
+
"repeat" => method(:repeat_guesses),
|
236
|
+
"sequence" => method(:sequence_guesses),
|
237
|
+
"regex" => method(:regex_guesses),
|
238
|
+
"date" => method(:date_guesses)
|
246
239
|
}
|
247
|
-
guesses = estimation_functions[match[
|
248
|
-
match[
|
249
|
-
match[
|
250
|
-
|
240
|
+
guesses = estimation_functions[match["pattern"]].call(match)
|
241
|
+
match["guesses"] = [guesses, min_guesses].max
|
242
|
+
match["guesses_log10"] = Math.log10(match["guesses"])
|
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.
|
264
|
-
min_guesses = if match[
|
255
|
+
min_guesses = if match["token"].length == 1
|
265
256
|
MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1
|
266
257
|
else
|
267
258
|
MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1
|
268
259
|
end
|
269
260
|
|
270
|
-
[guesses, min_guesses].max
|
261
|
+
[guesses, min_guesses].max.to_f
|
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
|
-
first_chr = match[
|
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
|
-
if !match[
|
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
|
@@ -301,126 +290,123 @@ module Zxcvbn
|
|
301
290
|
|
302
291
|
def self.regex_guesses(match)
|
303
292
|
char_class_bases = {
|
304
|
-
alpha_lower
|
305
|
-
alpha_upper
|
306
|
-
alpha
|
307
|
-
alphanumeric
|
308
|
-
digits
|
309
|
-
symbols
|
293
|
+
"alpha_lower" => 26,
|
294
|
+
"alpha_upper" => 26,
|
295
|
+
"alpha" => 52,
|
296
|
+
"alphanumeric" => 62,
|
297
|
+
"digits" => 10,
|
298
|
+
"symbols" => 33
|
310
299
|
}
|
311
|
-
if char_class_bases.
|
312
|
-
|
313
|
-
elsif match[
|
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
|
-
year_space = (match[
|
317
|
-
|
318
|
-
|
305
|
+
year_space = (match["regex_match"][0].to_i - REFERENCE_YEAR).abs
|
306
|
+
[year_space, MIN_YEAR_SPACE].max
|
307
|
+
|
319
308
|
end
|
320
309
|
end
|
321
310
|
|
322
311
|
def self.date_guesses(match)
|
323
312
|
# base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years
|
324
|
-
year_space = [(match[
|
313
|
+
year_space = [(match["year"] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
|
325
314
|
guesses = year_space * 365
|
326
|
-
|
315
|
+
separator = match["separator"]
|
316
|
+
if !["", nil].include?(separator)
|
327
317
|
# add factor of 4 for separator selection (one of ~4 choices)
|
328
318
|
guesses *= 4
|
329
319
|
end
|
330
|
-
|
320
|
+
guesses
|
331
321
|
end
|
332
322
|
|
333
|
-
KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[
|
323
|
+
KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"]).freeze
|
334
324
|
# slightly different for keypad/mac keypad, but close enough
|
335
|
-
KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[
|
325
|
+
KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"]).freeze
|
336
326
|
|
337
|
-
KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS[
|
338
|
-
KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS[
|
327
|
+
KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS["qwerty"].keys.size
|
328
|
+
KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS["keypad"].keys.size
|
339
329
|
|
340
330
|
def self.spatial_guesses(match)
|
341
|
-
if [
|
342
|
-
s = KEYBOARD_STARTING_POSITIONS
|
343
|
-
d = KEYBOARD_AVERAGE_DEGREE
|
331
|
+
if ["qwerty", "dvorak"].include?(match["graph"])
|
332
|
+
s = KEYBOARD_STARTING_POSITIONS
|
333
|
+
d = KEYBOARD_AVERAGE_DEGREE
|
344
334
|
else
|
345
|
-
s = KEYPAD_STARTING_POSITIONS
|
346
|
-
d = KEYPAD_AVERAGE_DEGREE
|
335
|
+
s = KEYPAD_STARTING_POSITIONS
|
336
|
+
d = KEYPAD_AVERAGE_DEGREE
|
347
337
|
end
|
348
|
-
guesses = 0
|
349
|
-
ll = match[
|
350
|
-
t = match[
|
338
|
+
guesses = 0.0
|
339
|
+
ll = match["token"].length
|
340
|
+
t = match["turns"]
|
351
341
|
# estimate the number of possible patterns w/ length ll or less with t turns or less.
|
352
342
|
(2..ll).each do |i|
|
353
343
|
possible_turns = [t, i - 1].min
|
354
344
|
(1..possible_turns).each do |j|
|
355
|
-
guesses +=
|
345
|
+
guesses += nck((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f**j.to_f)
|
356
346
|
end
|
357
347
|
end
|
358
348
|
# add extra guesses for shifted keys. (% instead of 5, A instead of a.)
|
359
349
|
# math is similar to extra guesses of l33t substitutions in dictionary matches.
|
360
|
-
if match[
|
361
|
-
ss = match[
|
362
|
-
uu = match[
|
350
|
+
if match["shifted_count"] && match["shifted_count"] != 0
|
351
|
+
ss = match["shifted_count"]
|
352
|
+
uu = match["token"].length - match["shifted_count"] # unshifted count
|
363
353
|
if ss == 0 || uu == 0
|
364
354
|
guesses *= 2
|
365
355
|
else
|
366
356
|
shifted_variations = 0
|
367
357
|
(1..[ss, uu].min).each do |i|
|
368
|
-
shifted_variations +=
|
358
|
+
shifted_variations += nck((ss + uu).to_f, i.to_f)
|
369
359
|
end
|
370
360
|
guesses *= shifted_variations
|
371
361
|
end
|
372
362
|
end
|
373
|
-
|
363
|
+
guesses
|
374
364
|
end
|
375
365
|
|
376
366
|
def self.dictionary_guesses(match)
|
377
|
-
match[
|
378
|
-
match[
|
379
|
-
match[
|
380
|
-
reversed_variations = match[
|
381
|
-
|
367
|
+
match["base_guesses"] = match["rank"] # keep these as properties for display purposes
|
368
|
+
match["uppercase_variations"] = uppercase_variations(match)
|
369
|
+
match["l33t_variations"] = l33t_variations(match)
|
370
|
+
reversed_variations = match["reversed"] && 2 || 1
|
371
|
+
match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
|
382
372
|
end
|
383
373
|
|
384
|
-
START_UPPER = /^[A-Z][^A-Z]
|
385
|
-
END_UPPER = /^[^A-Z]+[A-Z]
|
386
|
-
ALL_UPPER = /^[^a-z]
|
387
|
-
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
|
388
378
|
|
389
379
|
def self.uppercase_variations(match)
|
390
|
-
word = match[
|
391
|
-
if word.match?(ALL_LOWER) || word.downcase
|
392
|
-
|
393
|
-
end
|
380
|
+
word = match["token"]
|
381
|
+
return 1 if word.match?(ALL_LOWER) || word.downcase == word
|
382
|
+
|
394
383
|
# a capitalized word is the most common capitalization scheme,
|
395
384
|
# so it only doubles the search space (uncapitalized + capitalized).
|
396
385
|
# allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
|
397
386
|
[START_UPPER, END_UPPER, ALL_UPPER].each do |regex|
|
398
|
-
if word.match?(regex)
|
399
|
-
return 2
|
400
|
-
end
|
387
|
+
return 2 if word.match?(regex)
|
401
388
|
end
|
402
389
|
# otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
|
403
390
|
# with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
|
404
391
|
# the number of ways to lowercase U+L letters with L lowercase letters or less.
|
405
|
-
uu = word.
|
406
|
-
ll = word.
|
407
|
-
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
|
408
395
|
(1..[uu, ll].min).each do |i|
|
409
|
-
variations +=
|
396
|
+
variations += nck(uu + ll, i)
|
410
397
|
end
|
411
|
-
|
398
|
+
variations
|
412
399
|
end
|
413
400
|
|
414
401
|
def self.l33t_variations(match)
|
415
|
-
if !match[
|
416
|
-
|
417
|
-
end
|
402
|
+
return 1 if !match["l33t"]
|
403
|
+
|
418
404
|
variations = 1
|
419
|
-
match[
|
405
|
+
match["sub"].each do |subbed, unsubbed|
|
420
406
|
# lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
|
421
|
-
chrs = match[
|
422
|
-
ss = chrs.count{|chr| chr == subbed }
|
423
|
-
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 }
|
424
410
|
if ss == 0 || uu == 0
|
425
411
|
# for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
|
426
412
|
# treat that as doubling the space (attacker needs to try fully subbed chars in addition to
|
@@ -432,12 +418,12 @@ module Zxcvbn
|
|
432
418
|
p = [uu, ss].min
|
433
419
|
possibilities = 0
|
434
420
|
(1..p).each do |i|
|
435
|
-
possibilities +=
|
421
|
+
possibilities += nck(uu + ss, i)
|
436
422
|
end
|
437
423
|
variations *= possibilities
|
438
424
|
end
|
439
425
|
end
|
440
|
-
|
426
|
+
variations
|
441
427
|
end
|
442
428
|
end
|
443
429
|
end
|