zxcvbn 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: