zxcvbn 0.1.3 → 0.1.4

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.
@@ -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