zxcvbn 0.1.3 → 0.1.4

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,19 +113,13 @@ 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
@@ -138,9 +128,9 @@ module Zxcvbn
138
128
  end
139
129
 
140
130
  # helper: make bruteforce match objects spanning i to j, inclusive.
141
- make_bruteforce_match = -> (i, j) do
131
+ make_bruteforce_match = lambda do |i, j|
142
132
  return {
143
- "pattern" => 'bruteforce',
133
+ "pattern" => "bruteforce",
144
134
  "token" => password[i..j],
145
135
  "i" => i,
146
136
  "j" => j
@@ -148,7 +138,7 @@ module Zxcvbn
148
138
  end
149
139
 
150
140
  # helper: evaluate bruteforce matches ending at k.
151
- bruteforce_update = -> (k) do
141
+ bruteforce_update = lambda do |k|
152
142
  # see if a single bruteforce match spanning the k-prefix is optimal.
153
143
  m = make_bruteforce_match.call(0, k)
154
144
  update.call(m, 1)
@@ -156,16 +146,15 @@ module Zxcvbn
156
146
  # generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
157
147
  # see if adding these new matches to any of the sequences in optimal[i-1]
158
148
  # leads to new bests.
159
- m = make_bruteforce_match.call(i, k);
160
- optimal["m"][i-1].each do |l, last_m|
149
+ m = make_bruteforce_match.call(i, k)
150
+ optimal["m"][i - 1].each do |l, last_m|
161
151
  l = l.to_i
162
152
  # corner: an optimal sequence will never have two adjacent bruteforce matches.
163
153
  # it is strictly better to have a single bruteforce match spanning the same region:
164
154
  # same contribution to the guess product with a lower length.
165
155
  # --> safe to skip those cases.
166
- if last_m["pattern"] == 'bruteforce'
167
- next
168
- end
156
+ next if last_m["pattern"] == "bruteforce"
157
+
169
158
  # try adding m to this length-l sequence.
170
159
  update.call(m, l + 1)
171
160
  end
@@ -174,11 +163,11 @@ module Zxcvbn
174
163
 
175
164
  # helper: step backwards through optimal.m starting at the end,
176
165
  # constructing the final optimal match sequence.
177
- unwind = -> (n) do
166
+ unwind = lambda do |n2|
178
167
  optimal_match_sequence = []
179
- k = n - 1
168
+ k = n2 - 1
180
169
  # find the final best sequence length and score
181
- l, g = (optimal["g"][k] || []).min_by{|candidate_l, candidate_g| candidate_g || 0 }
170
+ l, _g = (optimal["g"][k] || []).min_by { |_candidate_l, candidate_g| candidate_g || 0 }
182
171
  while k >= 0
183
172
  m = optimal["m"][k][l]
184
173
  optimal_match_sequence.unshift(m)
@@ -191,7 +180,7 @@ module Zxcvbn
191
180
  (0...n).each do |k|
192
181
  matches_by_j[k].each do |m|
193
182
  if m["i"] > 0
194
- optimal["m"][m["i"] - 1].keys.each do |l|
183
+ optimal["m"][m["i"] - 1].each_key do |l|
195
184
  update.call(m, l + 1)
196
185
  end
197
186
  else
@@ -205,14 +194,14 @@ module Zxcvbn
205
194
  optimal_l = optimal_match_sequence.length
206
195
 
207
196
  # corner: empty password
208
- if password.length == 0
209
- guesses = 1
197
+ guesses = if password.empty?
198
+ 1
210
199
  else
211
- guesses = optimal["g"][n - 1][optimal_l]
200
+ optimal["g"][n - 1][optimal_l]
212
201
  end
213
202
 
214
203
  # final result object
215
- return {
204
+ {
216
205
  "password" => password,
217
206
  "guesses" => guesses,
218
207
  "guesses_log10" => Math.log10(guesses),
@@ -227,6 +216,7 @@ module Zxcvbn
227
216
  if match["guesses"]
228
217
  return match["guesses"] # a match's guess estimate doesn't change. cache it.
229
218
  end
219
+
230
220
  min_guesses = 1
231
221
  if match["token"].length < password.length
232
222
  min_guesses = if match["token"].length == 1
@@ -242,22 +232,20 @@ module Zxcvbn
242
232
  "repeat" => method(:repeat_guesses),
243
233
  "sequence" => method(:sequence_guesses),
244
234
  "regex" => method(:regex_guesses),
245
- "date" => method(:date_guesses),
235
+ "date" => method(:date_guesses)
246
236
  }
247
237
  guesses = estimation_functions[match["pattern"]].call(match)
248
238
  match["guesses"] = [guesses, min_guesses].max
249
239
  match["guesses_log10"] = Math.log10(match["guesses"])
250
- return match["guesses"]
240
+ match["guesses"]
251
241
  end
252
242
 
253
- MAX_VALUE = 2 ** 1024
243
+ MAX_VALUE = 2**1024
254
244
 
255
245
  def self.bruteforce_guesses(match)
256
- guesses = BRUTEFORCE_CARDINALITY ** match["token"].length
246
+ guesses = BRUTEFORCE_CARDINALITY**match["token"].length
257
247
  # 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
248
+ guesses = MAX_VALUE if guesses > MAX_VALUE
261
249
 
262
250
  # small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
263
251
  # submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
@@ -271,29 +259,27 @@ module Zxcvbn
271
259
  end
272
260
 
273
261
  def self.repeat_guesses(match)
274
- return match["base_guesses"] * match["repeat_count"]
262
+ match["base_guesses"] * match["repeat_count"]
275
263
  end
276
264
 
277
265
  def self.sequence_guesses(match)
278
266
  first_chr = match["token"][0]
279
267
  # lower guesses for obvious starting points
280
- if ['a', 'A', 'z', 'Z', '0', '1', '9'].include?(first_chr)
281
- base_guesses = 4
268
+ base_guesses = if ["a", "A", "z", "Z", "0", "1", "9"].include?(first_chr)
269
+ 4
270
+ elsif first_chr.match?(/\d/)
271
+ 10
282
272
  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
273
+ # could give a higher base for uppercase,
274
+ # assigning 26 to both upper and lower sequences is more conservative.
275
+ 26
290
276
  end
291
277
  if !match["ascending"]
292
278
  # need to try a descending sequence in addition to every ascending sequence ->
293
279
  # 2x guesses
294
280
  base_guesses *= 2
295
281
  end
296
- return base_guesses * match["token"].length
282
+ base_guesses * match["token"].length
297
283
  end
298
284
 
299
285
  MIN_YEAR_SPACE = 20
@@ -308,14 +294,14 @@ module Zxcvbn
308
294
  "digits" => 10,
309
295
  "symbols" => 33
310
296
  }
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'
297
+ if char_class_bases.key? match["regex_name"]
298
+ char_class_bases[match["regex_name"]]**match["token"].length
299
+ elsif match["regex_name"] == "recent_year"
314
300
  # conservative estimate of year space: num years from REFERENCE_YEAR.
315
301
  # if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
316
302
  year_space = (match["regex_match"][0].to_i - REFERENCE_YEAR).abs
317
- year_space = [year_space, MIN_YEAR_SPACE].max
318
- return year_space
303
+ [year_space, MIN_YEAR_SPACE].max
304
+
319
305
  end
320
306
  end
321
307
 
@@ -328,23 +314,23 @@ module Zxcvbn
328
314
  # add factor of 4 for separator selection (one of ~4 choices)
329
315
  guesses *= 4
330
316
  end
331
- return guesses
317
+ guesses
332
318
  end
333
319
 
334
- KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"])
320
+ KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"]).freeze
335
321
  # slightly different for keypad/mac keypad, but close enough
336
- KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"])
322
+ KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"]).freeze
337
323
 
338
324
  KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS["qwerty"].keys.size
339
325
  KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS["keypad"].keys.size
340
326
 
341
327
  def self.spatial_guesses(match)
342
- if ['qwerty', 'dvorak'].include?(match["graph"])
343
- s = KEYBOARD_STARTING_POSITIONS;
344
- d = KEYBOARD_AVERAGE_DEGREE;
328
+ if ["qwerty", "dvorak"].include?(match["graph"])
329
+ s = KEYBOARD_STARTING_POSITIONS
330
+ d = KEYBOARD_AVERAGE_DEGREE
345
331
  else
346
- s = KEYPAD_STARTING_POSITIONS;
347
- d = KEYPAD_AVERAGE_DEGREE;
332
+ s = KEYPAD_STARTING_POSITIONS
333
+ d = KEYPAD_AVERAGE_DEGREE
348
334
  end
349
335
  guesses = 0.0
350
336
  ll = match["token"].length
@@ -353,7 +339,7 @@ module Zxcvbn
353
339
  (2..ll).each do |i|
354
340
  possible_turns = [t, i - 1].min
355
341
  (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)
342
+ guesses += nck((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f**j.to_f)
357
343
  end
358
344
  end
359
345
  # add extra guesses for shifted keys. (% instead of 5, A instead of a.)
@@ -366,12 +352,12 @@ module Zxcvbn
366
352
  else
367
353
  shifted_variations = 0
368
354
  (1..[ss, uu].min).each do |i|
369
- shifted_variations += nCk((ss + uu).to_f, i.to_f)
355
+ shifted_variations += nck((ss + uu).to_f, i.to_f)
370
356
  end
371
357
  guesses *= shifted_variations
372
358
  end
373
359
  end
374
- return guesses
360
+ guesses
375
361
  end
376
362
 
377
363
  def self.dictionary_guesses(match)
@@ -379,49 +365,45 @@ module Zxcvbn
379
365
  match["uppercase_variations"] = uppercase_variations(match)
380
366
  match["l33t_variations"] = l33t_variations(match)
381
367
  reversed_variations = match["reversed"] && 2 || 1
382
- return match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
368
+ match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
383
369
  end
384
370
 
385
- START_UPPER = /^[A-Z][^A-Z]+$/
386
- END_UPPER = /^[^A-Z]+[A-Z]$/
387
- ALL_UPPER = /^[^a-z]+$/
388
- ALL_LOWER = /^[^A-Z]+$/
371
+ START_UPPER = /^[A-Z][^A-Z]+$/.freeze
372
+ END_UPPER = /^[^A-Z]+[A-Z]$/.freeze
373
+ ALL_UPPER = /^[^a-z]+$/.freeze
374
+ ALL_LOWER = /^[^A-Z]+$/.freeze
389
375
 
390
376
  def self.uppercase_variations(match)
391
377
  word = match["token"]
392
- if word.match?(ALL_LOWER) || word.downcase === word
393
- return 1
394
- end
378
+ return 1 if word.match?(ALL_LOWER) || word.downcase == word
379
+
395
380
  # a capitalized word is the most common capitalization scheme,
396
381
  # so it only doubles the search space (uncapitalized + capitalized).
397
382
  # allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe.
398
383
  [START_UPPER, END_UPPER, ALL_UPPER].each do |regex|
399
- if word.match?(regex)
400
- return 2
401
- end
384
+ return 2 if word.match?(regex)
402
385
  end
403
386
  # otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters
404
387
  # with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD),
405
388
  # 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;
389
+ uu = word.chars.count { |chr| chr.match?(/[A-Z]/) }
390
+ ll = word.chars.count { |chr| chr.match?(/[a-z]/) }
391
+ variations = 0
409
392
  (1..[uu, ll].min).each do |i|
410
- variations += nCk(uu + ll, i)
393
+ variations += nck(uu + ll, i)
411
394
  end
412
- return variations
395
+ variations
413
396
  end
414
397
 
415
398
  def self.l33t_variations(match)
416
- if !match["l33t"]
417
- return 1
418
- end
399
+ return 1 if !match["l33t"]
400
+
419
401
  variations = 1
420
402
  match["sub"].each do |subbed, unsubbed|
421
403
  # 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 }
404
+ chrs = match["token"].downcase.chars
405
+ ss = chrs.count { |chr| chr == subbed }
406
+ uu = chrs.count { |chr| chr == unsubbed }
425
407
  if ss == 0 || uu == 0
426
408
  # for this sub, password is either fully subbed (444) or fully unsubbed (aaa)
427
409
  # treat that as doubling the space (attacker needs to try fully subbed chars in addition to
@@ -433,12 +415,12 @@ module Zxcvbn
433
415
  p = [uu, ss].min
434
416
  possibilities = 0
435
417
  (1..p).each do |i|
436
- possibilities += nCk(uu + ss, i)
418
+ possibilities += nck(uu + ss, i)
437
419
  end
438
420
  variations *= possibilities
439
421
  end
440
422
  end
441
- return variations
423
+ variations
442
424
  end
443
425
  end
444
426
  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