zxcvbn 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +34 -2
- data/.travis.yml +1 -1
- data/CHANGELOG.md +5 -1
- data/Gemfile +9 -4
- data/Gemfile.lock +26 -1
- data/README.md +36 -36
- data/bin/console +5 -2
- data/lib/zxcvbn.rb +7 -9
- data/lib/zxcvbn/adjacency_graphs.rb +228 -224
- data/lib/zxcvbn/feedback.rb +68 -70
- data/lib/zxcvbn/frequency_lists.rb +9 -7
- data/lib/zxcvbn/matching.rb +244 -267
- data/lib/zxcvbn/scoring.rb +154 -168
- data/lib/zxcvbn/time_estimates.rb +18 -20
- data/lib/zxcvbn/version.rb +1 -1
- data/zxcvbn.gemspec +1 -1
- metadata +8 -7
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,127 +290,124 @@ 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 =
|
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 }
|
424
|
-
if ss
|
407
|
+
chrs = match["token"].downcase.chars
|
408
|
+
ss = chrs.count { |chr| chr == subbed }
|
409
|
+
uu = chrs.count { |chr| chr == unsubbed }
|
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
|
427
413
|
# unsubbed.)
|
@@ -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
|