zxcvbn 0.1.2 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 |key, neighbors|
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
- return average
13
+ average
14
14
  end
15
15
 
16
16
  BRUTEFORCE_CARDINALITY = 10
17
17
 
18
- MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000
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.nCk(n, k)
24
+ def self.nck(n, k) # rubocop:disable Naming/MethodParameterName
25
25
  # http://blog.plover.com/math/choose.html
26
- if k > n
27
- return 0.0
28
- end
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
- return r
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
- return 1
45
- end
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 = false)
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 = -> (m, l) do
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].each do |competing_l, competing_g|
127
- if competing_l > l
128
- next
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 = -> (i, j) do
135
+ make_bruteforce_match = lambda do |i, j|
142
136
  return {
143
- "pattern" => 'bruteforce',
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 = -> (k) do
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"] == 'bruteforce'
167
- next
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 = -> (n) do
169
+ unwind = lambda do |n2|
178
170
  optimal_match_sequence = []
179
- k = n - 1
171
+ k = n2 - 1
180
172
  # find the final best sequence length and score
181
- l, g = (optimal["g"][k] || []).min_by{|candidate_l, candidate_g| candidate_g || 0 }
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].keys.each do |l|
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.length == 0
209
- guesses = 1
200
+ guesses = if password.empty?
201
+ 1
210
202
  else
211
- guesses = optimal["g"][n - 1][optimal_l]
203
+ optimal["g"][n - 1][optimal_l]
212
204
  end
213
205
 
214
206
  # final result object
215
- return {
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
- return match["guesses"]
243
+ match["guesses"]
251
244
  end
252
245
 
253
- MAX_VALUE = 2 ** 1024
246
+ MAX_VALUE = 2**1024
254
247
 
255
248
  def self.bruteforce_guesses(match)
256
- guesses = BRUTEFORCE_CARDINALITY ** match["token"].length
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
- return match["base_guesses"] * match["repeat_count"]
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 ['a', 'A', 'z', 'Z', '0', '1', '9'].include?(first_chr)
281
- base_guesses = 4
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
- if first_chr.match?(/\d/)
284
- base_guesses = 10 # digits
285
- else
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
- return base_guesses * match["token"].length
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.has_key? match["regex_name"]
312
- return char_class_bases[match["regex_name"]] ** match["token"].length
313
- elsif match["regex_name"] == 'recent_year'
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
- year_space = [year_space, MIN_YEAR_SPACE].max
318
- return year_space
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
- return guesses
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 ['qwerty', 'dvorak'].include?(match["graph"])
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 += nCk((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f ** j.to_f)
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 += nCk((ss + uu).to_f, i.to_f)
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
- return guesses
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
- return match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
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 === word
393
- return 1
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.split("").count{|chr| chr.match?(/[A-Z]/)}
407
- ll = word.split("").count{|chr| chr.match?(/[a-z]/)}
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 += nCk(uu + ll, i)
396
+ variations += nck(uu + ll, i)
411
397
  end
412
- return variations
398
+ variations
413
399
  end
414
400
 
415
401
  def self.l33t_variations(match)
416
- if !match["l33t"]
417
- return 1
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.split('')
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 += nCk(uu + ss, i)
421
+ possibilities += nck(uu + ss, i)
437
422
  end
438
423
  variations *= possibilities
439
424
  end
440
425
  end
441
- return variations
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
- return {
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
- return 0
28
+ 0
29
29
  elsif guesses < 1e6 + delta
30
30
  # modest protection from throttled online attacks: "very guessable"
31
- return 1
31
+ 1
32
32
  elsif guesses < 1e8 + delta
33
33
  # modest protection from unthrottled online attacks: "somewhat guessable"
34
- return 2
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
- return 3
38
+ 3
39
39
  else
40
40
  # strong protection from offline attacks under same scenario: "very unguessable"
41
- return 4
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, 'less than a second']
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, 'centuries']
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