zxcvbn 0.1.2 → 0.1.7

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,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