zxcvbn 0.1.1 → 0.1.2

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.
@@ -7,9 +7,9 @@ module Zxcvbn
7
7
  def self.calc_average_degree(graph)
8
8
  average = 0
9
9
  graph.each do |key, neighbors|
10
- average += neighbors.count {|n| n }
10
+ average += neighbors.count {|n| n }.to_f
11
11
  end
12
- average /= graph.keys.size
12
+ average /= graph.keys.size.to_f
13
13
  return average
14
14
  end
15
15
 
@@ -24,16 +24,16 @@ module Zxcvbn
24
24
  def self.nCk(n, k)
25
25
  # http://blog.plover.com/math/choose.html
26
26
  if k > n
27
- return 0
27
+ return 0.0
28
28
  end
29
29
  if k == 0
30
- return 1
30
+ return 1.0
31
31
  end
32
- r = 1
32
+ r = 1.0
33
33
  (1..k).each do |d|
34
34
  r *= n
35
35
  r /= d
36
- n -= 1
36
+ n -= 1.0
37
37
  end
38
38
  return r
39
39
  end
@@ -43,7 +43,7 @@ module Zxcvbn
43
43
  if n < 2
44
44
  return 1
45
45
  end
46
- return (2..10).reduce(&:*)
46
+ return (2..n).reduce(&:*)
47
47
  end
48
48
 
49
49
  # ------------------------------------------------------------------------------
@@ -83,12 +83,12 @@ module Zxcvbn
83
83
  # partition matches into sublists according to ending index j
84
84
  matches_by_j = (0...n).map { [] }
85
85
  matches.each do |m|
86
- matches_by_j[m[:j]] << m
86
+ matches_by_j[m["j"]] << m
87
87
  end
88
88
 
89
89
  # small detail: for deterministic output, sort each sublist by i.
90
90
  matches_by_j.each do |lst|
91
- lst.sort_by!{|m| m[:i] }
91
+ lst.sort_by!{|m| m["i"] }
92
92
  end
93
93
 
94
94
  optimal = {
@@ -96,24 +96,24 @@ module Zxcvbn
96
96
  # password prefix up to k, inclusive.
97
97
  # if there is no length-l sequence that scores better (fewer guesses) than
98
98
  # a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
99
- m: (0...n).map { {} },
99
+ "m" => (0...n).map { {} },
100
100
  # same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
101
101
  # optimal.pi allows for fast (non-looping) updates to the minimization function.
102
- pi: (0...n).map { {} },
102
+ "pi" => (0...n).map { {} },
103
103
  # same structure as optimal.m -- holds the overall metric.
104
- g: (0...n).map { {} },
104
+ "g" => (0...n).map { {} },
105
105
  }
106
106
 
107
107
  # helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
108
108
  # than previously encountered sequences, updating state if so.
109
109
  update = -> (m, l) do
110
- k = m[:j]
110
+ k = m["j"]
111
111
  pi = estimate_guesses(m, password)
112
112
  if l > 1
113
113
  # we're considering a length-l sequence ending with match m:
114
114
  # obtain the product term in the minimization function by multiplying m's guesses
115
115
  # by the product of the length-(l-1) sequence ending just before m, at m.i - 1.
116
- pi *= optimal[:pi][m[:i] - 1][l - 1]
116
+ pi *= optimal["pi"][m["i"] - 1][l - 1]
117
117
  end
118
118
  # calculate the minimization func
119
119
  g = factorial(l) * pi
@@ -123,7 +123,7 @@ module Zxcvbn
123
123
  # update state if new best.
124
124
  # first see if any competing sequences covering this prefix, with l or fewer matches,
125
125
  # fare better than this sequence. if so, skip it and return.
126
- optimal[:g][k].each do |competing_l, competing_g|
126
+ optimal["g"][k].each do |competing_l, competing_g|
127
127
  if competing_l > l
128
128
  next
129
129
  end
@@ -132,18 +132,18 @@ module Zxcvbn
132
132
  end
133
133
  end
134
134
  # this sequence might be part of the final optimal sequence.
135
- optimal[:g][k][l] = g
136
- optimal[:m][k][l] = m
137
- optimal[:pi][k][l] = pi
135
+ optimal["g"][k][l] = g
136
+ optimal["m"][k][l] = m
137
+ optimal["pi"][k][l] = pi
138
138
  end
139
139
 
140
140
  # helper: make bruteforce match objects spanning i to j, inclusive.
141
141
  make_bruteforce_match = -> (i, j) do
142
142
  return {
143
- pattern: 'bruteforce',
144
- token: password[i..j],
145
- i: i,
146
- j: j
143
+ "pattern" => 'bruteforce',
144
+ "token" => password[i..j],
145
+ "i" => i,
146
+ "j" => j
147
147
  }
148
148
  end
149
149
 
@@ -157,13 +157,13 @@ module Zxcvbn
157
157
  # see if adding these new matches to any of the sequences in optimal[i-1]
158
158
  # leads to new bests.
159
159
  m = make_bruteforce_match.call(i, k);
160
- optimal[:m][i-1].each do |l, last_m|
160
+ optimal["m"][i-1].each do |l, last_m|
161
161
  l = l.to_i
162
162
  # corner: an optimal sequence will never have two adjacent bruteforce matches.
163
163
  # it is strictly better to have a single bruteforce match spanning the same region:
164
164
  # same contribution to the guess product with a lower length.
165
165
  # --> safe to skip those cases.
166
- if last_m[:pattern] == 'bruteforce'
166
+ if last_m["pattern"] == 'bruteforce'
167
167
  next
168
168
  end
169
169
  # try adding m to this length-l sequence.
@@ -178,11 +178,11 @@ module Zxcvbn
178
178
  optimal_match_sequence = []
179
179
  k = n - 1
180
180
  # find the final best sequence length and score
181
- l, g = (optimal[:g][k] || []).min_by{|candidate_l, candidate_g| candidate_g || 0 }
181
+ l, g = (optimal["g"][k] || []).min_by{|candidate_l, candidate_g| candidate_g || 0 }
182
182
  while k >= 0
183
- m = optimal[:m][k][l]
183
+ m = optimal["m"][k][l]
184
184
  optimal_match_sequence.unshift(m)
185
- k = m[:i] - 1
185
+ k = m["i"] - 1
186
186
  l -= 1
187
187
  end
188
188
  return optimal_match_sequence
@@ -190,8 +190,8 @@ module Zxcvbn
190
190
 
191
191
  (0...n).each do |k|
192
192
  matches_by_j[k].each do |m|
193
- if m[:i] > 0
194
- optimal[:m][m[:i] - 1].keys.each do |l|
193
+ if m["i"] > 0
194
+ optimal["m"][m["i"] - 1].keys.each do |l|
195
195
  update.call(m, l + 1)
196
196
  end
197
197
  else
@@ -208,15 +208,15 @@ module Zxcvbn
208
208
  if password.length == 0
209
209
  guesses = 1
210
210
  else
211
- guesses = optimal[:g][n - 1][optimal_l]
211
+ guesses = optimal["g"][n - 1][optimal_l]
212
212
  end
213
213
 
214
214
  # final result object
215
215
  return {
216
- password: password,
217
- guesses: guesses,
218
- guesses_log10: Math.log10(guesses),
219
- sequence: optimal_match_sequence
216
+ "password" => password,
217
+ "guesses" => guesses,
218
+ "guesses_log10" => Math.log10(guesses),
219
+ "sequence" => optimal_match_sequence
220
220
  }
221
221
  end
222
222
 
@@ -224,36 +224,36 @@ module Zxcvbn
224
224
  # guess estimation -- one function per match pattern ---------------------------
225
225
  # ------------------------------------------------------------------------------
226
226
  def self.estimate_guesses(match, password)
227
- if match[:guesses]
228
- return match[:guesses] # a match's guess estimate doesn't change. cache it.
227
+ if match["guesses"]
228
+ return match["guesses"] # a match's guess estimate doesn't change. cache it.
229
229
  end
230
230
  min_guesses = 1
231
- if match[:token].length < password.length
232
- min_guesses = if match[:token].length == 1
231
+ if match["token"].length < password.length
232
+ min_guesses = if match["token"].length == 1
233
233
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR
234
234
  else
235
235
  MIN_SUBMATCH_GUESSES_MULTI_CHAR
236
236
  end
237
237
  end
238
238
  estimation_functions = {
239
- bruteforce: method(:bruteforce_guesses),
240
- dictionary: method(:dictionary_guesses),
241
- spatial: method(:spatial_guesses),
242
- repeat: method(:repeat_guesses),
243
- sequence: method(:sequence_guesses),
244
- regex: method(:regex_guesses),
245
- date: method(:date_guesses),
239
+ "bruteforce" => method(:bruteforce_guesses),
240
+ "dictionary" => method(:dictionary_guesses),
241
+ "spatial" => method(:spatial_guesses),
242
+ "repeat" => method(:repeat_guesses),
243
+ "sequence" => method(:sequence_guesses),
244
+ "regex" => method(:regex_guesses),
245
+ "date" => method(:date_guesses),
246
246
  }
247
- guesses = estimation_functions[match[:pattern].to_sym].call(match)
248
- match[:guesses] = [guesses, min_guesses].max
249
- match[:guesses_log10] = Math.log10(match[:guesses])
250
- return match[:guesses]
247
+ guesses = estimation_functions[match["pattern"]].call(match)
248
+ match["guesses"] = [guesses, min_guesses].max
249
+ match["guesses_log10"] = Math.log10(match["guesses"])
250
+ return match["guesses"]
251
251
  end
252
252
 
253
253
  MAX_VALUE = 2 ** 1024
254
254
 
255
255
  def self.bruteforce_guesses(match)
256
- guesses = BRUTEFORCE_CARDINALITY ** match[:token].length
256
+ guesses = BRUTEFORCE_CARDINALITY ** match["token"].length
257
257
  # trying to match JS behaviour here setting a MAX_VALUE to try to acheieve same values as JS library.
258
258
  if guesses > MAX_VALUE
259
259
  guesses = MAX_VALUE
@@ -261,21 +261,21 @@ module Zxcvbn
261
261
 
262
262
  # small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
263
263
  # submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
264
- min_guesses = if match[:token].length == 1
264
+ min_guesses = if match["token"].length == 1
265
265
  MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1
266
266
  else
267
267
  MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1
268
268
  end
269
269
 
270
- [guesses, min_guesses].max
270
+ [guesses, min_guesses].max.to_f
271
271
  end
272
272
 
273
273
  def self.repeat_guesses(match)
274
- return match[:base_guesses] * match[:repeat_count]
274
+ return match["base_guesses"] * match["repeat_count"]
275
275
  end
276
276
 
277
277
  def self.sequence_guesses(match)
278
- first_chr = match[:token][0]
278
+ first_chr = match["token"][0]
279
279
  # lower guesses for obvious starting points
280
280
  if ['a', 'A', 'z', 'Z', '0', '1', '9'].include?(first_chr)
281
281
  base_guesses = 4
@@ -288,12 +288,12 @@ module Zxcvbn
288
288
  base_guesses = 26
289
289
  end
290
290
  end
291
- if !match[:ascending]
291
+ if !match["ascending"]
292
292
  # need to try a descending sequence in addition to every ascending sequence ->
293
293
  # 2x guesses
294
294
  base_guesses *= 2
295
295
  end
296
- return base_guesses * match[:token].length
296
+ return base_guesses * match["token"].length
297
297
  end
298
298
 
299
299
  MIN_YEAR_SPACE = 20
@@ -301,19 +301,19 @@ module Zxcvbn
301
301
 
302
302
  def self.regex_guesses(match)
303
303
  char_class_bases = {
304
- alpha_lower: 26,
305
- alpha_upper: 26,
306
- alpha: 52,
307
- alphanumeric: 62,
308
- digits: 10,
309
- symbols: 33
304
+ "alpha_lower" => 26,
305
+ "alpha_upper" => 26,
306
+ "alpha" => 52,
307
+ "alphanumeric" => 62,
308
+ "digits" => 10,
309
+ "symbols" => 33
310
310
  }
311
- if char_class_bases.has_key? match[:regex_name].to_sym
312
- return char_class_bases[match[:regex_name].to_sym] ** match[:token].length
313
- elsif match[:regex_name] == 'recent_year'
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'
314
314
  # conservative estimate of year space: num years from REFERENCE_YEAR.
315
315
  # if year is close to REFERENCE_YEAR, estimate a year space of MIN_YEAR_SPACE.
316
- year_space = (match[:regex_match[0]].to_i - REFERENCE_YEAR).abs
316
+ year_space = (match["regex_match"][0].to_i - REFERENCE_YEAR).abs
317
317
  year_space = [year_space, MIN_YEAR_SPACE].max
318
318
  return year_space
319
319
  end
@@ -321,51 +321,52 @@ module Zxcvbn
321
321
 
322
322
  def self.date_guesses(match)
323
323
  # base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years
324
- year_space = [(match[:year] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
324
+ year_space = [(match["year"] - REFERENCE_YEAR).abs, MIN_YEAR_SPACE].max
325
325
  guesses = year_space * 365
326
- if match[:separator]
326
+ separator = match["separator"]
327
+ if !["", nil].include?(separator)
327
328
  # add factor of 4 for separator selection (one of ~4 choices)
328
329
  guesses *= 4
329
330
  end
330
331
  return guesses
331
332
  end
332
333
 
333
- KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[:qwerty])
334
+ KEYBOARD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["qwerty"])
334
335
  # slightly different for keypad/mac keypad, but close enough
335
- KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS[:keypad])
336
+ KEYPAD_AVERAGE_DEGREE = calc_average_degree(ADJACENCY_GRAPHS["keypad"])
336
337
 
337
- KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS[:qwerty].keys.size
338
- KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS[:keypad].keys.size
338
+ KEYBOARD_STARTING_POSITIONS = ADJACENCY_GRAPHS["qwerty"].keys.size
339
+ KEYPAD_STARTING_POSITIONS = ADJACENCY_GRAPHS["keypad"].keys.size
339
340
 
340
341
  def self.spatial_guesses(match)
341
- if ['qwerty', 'dvorak'].include?(match[:graph])
342
+ if ['qwerty', 'dvorak'].include?(match["graph"])
342
343
  s = KEYBOARD_STARTING_POSITIONS;
343
344
  d = KEYBOARD_AVERAGE_DEGREE;
344
345
  else
345
346
  s = KEYPAD_STARTING_POSITIONS;
346
347
  d = KEYPAD_AVERAGE_DEGREE;
347
348
  end
348
- guesses = 0
349
- ll = match[:token].length
350
- t = match[:turns]
349
+ guesses = 0.0
350
+ ll = match["token"].length
351
+ t = match["turns"]
351
352
  # estimate the number of possible patterns w/ length ll or less with t turns or less.
352
353
  (2..ll).each do |i|
353
354
  possible_turns = [t, i - 1].min
354
355
  (1..possible_turns).each do |j|
355
- guesses += nCk(i - 1, j - 1) * s * (d ** j)
356
+ guesses += nCk((i - 1).to_f, (j - 1).to_f) * s.to_f * (d.to_f ** j.to_f)
356
357
  end
357
358
  end
358
359
  # add extra guesses for shifted keys. (% instead of 5, A instead of a.)
359
360
  # math is similar to extra guesses of l33t substitutions in dictionary matches.
360
- if match[:shifted_count]
361
- ss = match[:shifted_count]
362
- uu = match[:token].length - match[:shifted_count] # unshifted count
361
+ if match["shifted_count"] && match["shifted_count"] != 0
362
+ ss = match["shifted_count"]
363
+ uu = match["token"].length - match["shifted_count"] # unshifted count
363
364
  if ss == 0 || uu == 0
364
365
  guesses *= 2
365
366
  else
366
367
  shifted_variations = 0
367
368
  (1..[ss, uu].min).each do |i|
368
- shifted_variations += nCk(ss + uu, i)
369
+ shifted_variations += nCk((ss + uu).to_f, i.to_f)
369
370
  end
370
371
  guesses *= shifted_variations
371
372
  end
@@ -374,11 +375,11 @@ module Zxcvbn
374
375
  end
375
376
 
376
377
  def self.dictionary_guesses(match)
377
- match[:base_guesses] = match[:rank] # keep these as properties for display purposes
378
- match[:uppercase_variations] = uppercase_variations(match)
379
- match[:l33t_variations] = l33t_variations(match)
380
- reversed_variations = match[:reversed] && 2 || 1
381
- return match[:base_guesses] * match[:uppercase_variations] * match[:l33t_variations] * reversed_variations
378
+ match["base_guesses"] = match["rank"] # keep these as properties for display purposes
379
+ match["uppercase_variations"] = uppercase_variations(match)
380
+ match["l33t_variations"] = l33t_variations(match)
381
+ reversed_variations = match["reversed"] && 2 || 1
382
+ return match["base_guesses"] * match["uppercase_variations"] * match["l33t_variations"] * reversed_variations
382
383
  end
383
384
 
384
385
  START_UPPER = /^[A-Z][^A-Z]+$/
@@ -387,7 +388,7 @@ module Zxcvbn
387
388
  ALL_LOWER = /^[^A-Z]+$/
388
389
 
389
390
  def self.uppercase_variations(match)
390
- word = match[:token]
391
+ word = match["token"]
391
392
  if word.match?(ALL_LOWER) || word.downcase === word
392
393
  return 1
393
394
  end
@@ -412,13 +413,13 @@ module Zxcvbn
412
413
  end
413
414
 
414
415
  def self.l33t_variations(match)
415
- if !match[:l33t]
416
+ if !match["l33t"]
416
417
  return 1
417
418
  end
418
419
  variations = 1
419
- match[:sub].each do |subbed, unsubbed|
420
+ match["sub"].each do |subbed, unsubbed|
420
421
  # lower-case match.token before calculating: capitalization shouldn't affect l33t calc.
421
- chrs = match[:token].downcase.split('')
422
+ chrs = match["token"].downcase.split('')
422
423
  ss = chrs.count{|chr| chr == subbed }
423
424
  uu = chrs.count{|chr| chr == unsubbed }
424
425
  if ss == 0 || uu == 0
@@ -4,10 +4,10 @@ module Zxcvbn
4
4
  module TimeEstimates
5
5
  def self.estimate_attack_times(guesses)
6
6
  crack_times_seconds = {
7
- online_throttling_100_per_hour: guesses / (100.0 / 3600.0),
8
- online_no_throttling_10_per_second: guesses / 10.0,
9
- offline_slow_hashing_1e4_per_second: guesses / 1e4,
10
- offline_fast_hashing_1e10_per_second: guesses / 1e10
7
+ "online_throttling_100_per_hour" => guesses / (100.0 / 3600.0),
8
+ "online_no_throttling_10_per_second" => guesses / 10.0,
9
+ "offline_slow_hashing_1e4_per_second" => guesses / 1e4,
10
+ "offline_fast_hashing_1e10_per_second" => guesses / 1e10
11
11
  }
12
12
  crack_times_display = {};
13
13
  crack_times_seconds.each do |scenario, seconds|
@@ -15,9 +15,9 @@ module Zxcvbn
15
15
  end
16
16
 
17
17
  return {
18
- crack_times_seconds: crack_times_seconds,
19
- crack_times_display: crack_times_display,
20
- score: guesses_to_score(guesses),
18
+ "crack_times_seconds" => crack_times_seconds,
19
+ "crack_times_display" => crack_times_display,
20
+ "score" => guesses_to_score(guesses),
21
21
  }
22
22
  end
23
23
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zxcvbn
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zxcvbn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Santos
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-16 00:00:00.000000000 Z
11
+ date: 2021-05-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby port of Dropbox's zxcvbn.js
14
14
  email: