zxcvbn-ruby 1.2.0 → 1.4.0

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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -2
  3. data/README.md +3 -12
  4. data/lib/zxcvbn/clock.rb +11 -0
  5. data/lib/zxcvbn/crack_time.rb +14 -14
  6. data/lib/zxcvbn/data.rb +42 -9
  7. data/lib/zxcvbn/dictionary_ranker.rb +7 -6
  8. data/lib/zxcvbn/entropy.rb +52 -48
  9. data/lib/zxcvbn/feedback.rb +2 -0
  10. data/lib/zxcvbn/feedback_giver.rb +4 -3
  11. data/lib/zxcvbn/match.rb +18 -4
  12. data/lib/zxcvbn/matchers/date.rb +26 -21
  13. data/lib/zxcvbn/matchers/dictionary.rb +48 -13
  14. data/lib/zxcvbn/matchers/digits.rb +3 -1
  15. data/lib/zxcvbn/matchers/l33t.rb +56 -38
  16. data/lib/zxcvbn/matchers/new_l33t.rb +30 -32
  17. data/lib/zxcvbn/matchers/regex_helpers.rb +6 -4
  18. data/lib/zxcvbn/matchers/repeat.rb +8 -8
  19. data/lib/zxcvbn/matchers/sequences.rb +18 -18
  20. data/lib/zxcvbn/matchers/spatial.rb +26 -24
  21. data/lib/zxcvbn/matchers/year.rb +4 -2
  22. data/lib/zxcvbn/math.rb +12 -8
  23. data/lib/zxcvbn/omnimatch.rb +5 -2
  24. data/lib/zxcvbn/password_strength.rb +6 -4
  25. data/lib/zxcvbn/score.rb +3 -1
  26. data/lib/zxcvbn/scorer.rb +26 -23
  27. data/lib/zxcvbn/tester.rb +2 -2
  28. data/lib/zxcvbn/trie.rb +44 -0
  29. data/lib/zxcvbn/version.rb +1 -1
  30. data/lib/zxcvbn.rb +4 -2
  31. data/sig/README.md +65 -0
  32. data/sig/zxcvbn/crack_time.rbs +13 -0
  33. data/sig/zxcvbn/data.rbs +31 -0
  34. data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
  35. data/sig/zxcvbn/entropy.rbs +33 -0
  36. data/sig/zxcvbn/feedback.rbs +8 -0
  37. data/sig/zxcvbn/feedback_giver.rbs +13 -0
  38. data/sig/zxcvbn/match.rbs +38 -0
  39. data/sig/zxcvbn/math.rbs +13 -0
  40. data/sig/zxcvbn/omnimatch.rbs +16 -0
  41. data/sig/zxcvbn/password_strength.rbs +10 -0
  42. data/sig/zxcvbn/score.rbs +15 -0
  43. data/sig/zxcvbn/scorer.rbs +20 -0
  44. data/sig/zxcvbn/tester.rbs +17 -0
  45. data/sig/zxcvbn/trie.rbs +13 -0
  46. data/sig/zxcvbn.rbs +10 -0
  47. metadata +27 -105
  48. data/.github/workflows/ci.yml +0 -23
  49. data/.gitignore +0 -18
  50. data/.rspec +0 -1
  51. data/CODE_OF_CONDUCT.md +0 -130
  52. data/Gemfile +0 -10
  53. data/Guardfile +0 -26
  54. data/Rakefile +0 -22
  55. data/spec/dictionary_ranker_spec.rb +0 -12
  56. data/spec/feedback_giver_spec.rb +0 -212
  57. data/spec/matchers/date_spec.rb +0 -109
  58. data/spec/matchers/dictionary_spec.rb +0 -30
  59. data/spec/matchers/digits_spec.rb +0 -15
  60. data/spec/matchers/l33t_spec.rb +0 -87
  61. data/spec/matchers/repeat_spec.rb +0 -18
  62. data/spec/matchers/sequences_spec.rb +0 -21
  63. data/spec/matchers/spatial_spec.rb +0 -20
  64. data/spec/matchers/year_spec.rb +0 -15
  65. data/spec/omnimatch_spec.rb +0 -24
  66. data/spec/scorer_spec.rb +0 -5
  67. data/spec/scoring/crack_time_spec.rb +0 -106
  68. data/spec/scoring/entropy_spec.rb +0 -216
  69. data/spec/scoring/math_spec.rb +0 -135
  70. data/spec/spec_helper.rb +0 -54
  71. data/spec/support/js_helpers.rb +0 -34
  72. data/spec/support/js_source/adjacency_graphs.js +0 -8
  73. data/spec/support/js_source/compiled.js +0 -1188
  74. data/spec/support/js_source/frequency_lists.js +0 -10
  75. data/spec/support/js_source/init.coffee +0 -63
  76. data/spec/support/js_source/init.js +0 -95
  77. data/spec/support/js_source/matching.coffee +0 -444
  78. data/spec/support/js_source/matching.js +0 -685
  79. data/spec/support/js_source/scoring.coffee +0 -270
  80. data/spec/support/js_source/scoring.js +0 -390
  81. data/spec/support/matcher.rb +0 -35
  82. data/spec/tester_spec.rb +0 -99
  83. data/spec/zxcvbn_spec.rb +0 -24
  84. data/zxcvbn-ruby.gemspec +0 -33
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/match'
2
4
 
3
5
  module Zxcvbn
@@ -6,31 +8,64 @@ module Zxcvbn
6
8
  # the lowercased password in the dictionary
7
9
 
8
10
  class Dictionary
9
- def initialize(name, ranked_dictionary)
11
+ def initialize(name, ranked_dictionary, trie = nil)
10
12
  @name = name
11
13
  @ranked_dictionary = ranked_dictionary
14
+ @trie = trie
12
15
  end
13
16
 
14
17
  def matches(password)
18
+ lowercased_password = password.downcase
19
+
20
+ if @trie
21
+ trie_matches(password, lowercased_password)
22
+ else
23
+ hash_matches(password, lowercased_password)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def trie_matches(password, lowercased_password)
30
+ results = []
31
+
32
+ (0...password.length).each do |i|
33
+ @trie.search_prefixes(lowercased_password, i).each do |word, rank, start, ending|
34
+ results << build_match(word, password.slice(start, ending - start + 1), start, ending, rank)
35
+ end
36
+ end
37
+
38
+ results
39
+ end
40
+
41
+ def hash_matches(password, lowercased_password)
15
42
  results = []
16
43
  password_length = password.length
17
- lowercased_password = password.downcase
44
+
18
45
  (0..password_length).each do |i|
19
46
  (i...password_length).each do |j|
20
- word = lowercased_password[i..j]
21
- if @ranked_dictionary.has_key?(word)
22
- results << Match.new(:matched_word => word,
23
- :token => password[i..j],
24
- :i => i,
25
- :j => j,
26
- :rank => @ranked_dictionary[word],
27
- :pattern => 'dictionary',
28
- :dictionary_name => @name)
29
- end
47
+ length = j - i + 1
48
+ word = lowercased_password.slice(i, length)
49
+ next unless @ranked_dictionary.key?(word)
50
+
51
+ results << build_match(word, password.slice(i, length), i, j, @ranked_dictionary[word])
30
52
  end
31
53
  end
54
+
32
55
  results
33
56
  end
57
+
58
+ def build_match(matched_word, token, start_pos, end_pos, rank)
59
+ Match.new(
60
+ matched_word: matched_word,
61
+ token: token,
62
+ i: start_pos,
63
+ j: end_pos,
64
+ rank: rank,
65
+ pattern: 'dictionary',
66
+ dictionary_name: @name
67
+ )
68
+ end
34
69
  end
35
70
  end
36
- end
71
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/matchers/regex_helpers'
2
4
 
3
5
  module Zxcvbn
@@ -5,7 +7,7 @@ module Zxcvbn
5
7
  class Digits
6
8
  include RegexHelpers
7
9
 
8
- DIGITS_REGEX = /\d{3,}/
10
+ DIGITS_REGEX = /\d{3,}/.freeze
9
11
 
10
12
  def matches(password)
11
13
  result = []
@@ -1,20 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
1
5
  module Zxcvbn
2
6
  module Matchers
3
7
  class L33t
4
8
  L33T_TABLE = {
5
- 'a' => ['4', '@'],
6
- 'b' => ['8'],
7
- 'c' => ['(', '{', '[', '<'],
8
- 'e' => ['3'],
9
- 'g' => ['6', '9'],
10
- 'i' => ['1', '!', '|'],
11
- 'l' => ['1', '|', '7'],
12
- 'o' => ['0'],
13
- 's' => ['$', '5'],
14
- 't' => ['+', '7'],
15
- 'x' => ['%'],
16
- 'z' => ['2']
17
- }
9
+ 'a' => ['4', '@'].freeze,
10
+ 'b' => ['8'].freeze,
11
+ 'c' => ['(', '{', '[', '<'].freeze,
12
+ 'e' => ['3'].freeze,
13
+ 'g' => ['6', '9'].freeze,
14
+ 'i' => ['1', '!', '|'].freeze,
15
+ 'l' => ['1', '|', '7'].freeze,
16
+ 'o' => ['0'].freeze,
17
+ 's' => ['$', '5'].freeze,
18
+ 't' => ['+', '7'].freeze,
19
+ 'x' => ['%'].freeze,
20
+ 'z' => ['2'].freeze
21
+ }.freeze
18
22
 
19
23
  def initialize(dictionary_matchers)
20
24
  @dictionary_matchers = dictionary_matchers
@@ -23,24 +27,17 @@ module Zxcvbn
23
27
  def matches(password)
24
28
  matches = []
25
29
  lowercased_password = password.downcase
26
- combinations_to_try = l33t_subs(relevent_l33t_subtable(lowercased_password))
30
+ relevent_subtable = relevent_l33t_subtable(lowercased_password)
31
+
32
+ # Early bailout: if no l33t characters present, return empty matches
33
+ return matches if relevent_subtable.empty?
34
+
35
+ combinations_to_try = l33t_subs(relevent_subtable)
27
36
  combinations_to_try.each do |substitution|
28
37
  @dictionary_matchers.each do |matcher|
29
38
  subbed_password = translate(lowercased_password, substitution)
30
39
  matcher.matches(subbed_password).each do |match|
31
- token = password[match.i..match.j]
32
- next if token.downcase == match.matched_word.downcase
33
- match_substitutions = {}
34
- substitution.each do |s, letter|
35
- match_substitutions[s] = letter if token.include?(s)
36
- end
37
- match.l33t = true
38
- match.token = password[match.i..match.j]
39
- match.sub = match_substitutions
40
- match.sub_display = match_substitutions.map do |k, v|
41
- "#{k} -> #{v}"
42
- end.join(', ')
43
- matches << match
40
+ process_match(match, password, substitution, matches)
44
41
  end
45
42
  end
46
43
  end
@@ -48,9 +45,11 @@ module Zxcvbn
48
45
  end
49
46
 
50
47
  def translate(password, sub)
51
- password.split('').map do |chr|
52
- sub[chr] || chr
53
- end.join
48
+ result = String.new
49
+ password.each_char do |chr|
50
+ result << (sub[chr] || chr)
51
+ end
52
+ result
54
53
  end
55
54
 
56
55
  def relevent_l33t_subtable(password)
@@ -77,8 +76,29 @@ module Zxcvbn
77
76
  new_subs
78
77
  end
79
78
 
79
+ private
80
+
81
+ def process_match(match, password, substitution, matches)
82
+ length = match.j - match.i + 1
83
+ token = password.slice(match.i, length)
84
+ return if token.downcase == match.matched_word.downcase
85
+
86
+ match_substitutions = {}
87
+ substitution.each do |s, letter|
88
+ match_substitutions[s] = letter if token.include?(s)
89
+ end
90
+ match.l33t = true
91
+ match.token = token
92
+ match.sub = match_substitutions
93
+ match.sub_display = match_substitutions.map do |k, v|
94
+ "#{k} -> #{v}"
95
+ end.join(', ')
96
+ matches << match
97
+ end
98
+
80
99
  def find_substitutions(subs, table, keys)
81
100
  return subs if keys.empty?
101
+
82
102
  first_key = keys[0]
83
103
  rest_keys = keys[1..-1]
84
104
  next_subs = []
@@ -109,14 +129,12 @@ module Zxcvbn
109
129
 
110
130
  def dedup(subs)
111
131
  deduped = []
112
- members = []
132
+ seen = Set.new
113
133
  subs.each do |sub|
114
- assoc = sub.dup
115
-
116
- assoc.sort! rescue debugger
117
- label = assoc.map{|k, v| "#{k},#{v}"}.join('-')
118
- unless members.include?(label)
119
- members << label
134
+ # Sort and convert to hash for consistent comparison
135
+ sorted_sub = sub.sort.to_h
136
+ unless seen.include?(sorted_sub)
137
+ seen.add(sorted_sub)
120
138
  deduped << sub
121
139
  end
122
140
  end
@@ -124,4 +142,4 @@ module Zxcvbn
124
142
  end
125
143
  end
126
144
  end
127
- end
145
+ end
@@ -1,20 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class L33t
4
6
  L33T_TABLE = {
5
- 'a' => ['4', '@'],
6
- 'b' => ['8'],
7
- 'c' => ['(', '{', '[', '<'],
8
- 'e' => ['3'],
9
- 'g' => ['6', '9'],
10
- 'i' => ['1', '!', '|'],
11
- 'l' => ['1', '|', '7'],
12
- 'o' => ['0'],
13
- 's' => ['$', '5'],
14
- 't' => ['+', '7'],
15
- 'x' => ['%'],
16
- 'z' => ['2']
17
- }
7
+ 'a' => ['4', '@'].freeze,
8
+ 'b' => ['8'].freeze,
9
+ 'c' => ['(', '{', '[', '<'].freeze,
10
+ 'e' => ['3'].freeze,
11
+ 'g' => ['6', '9'].freeze,
12
+ 'i' => ['1', '!', '|'].freeze,
13
+ 'l' => ['1', '|', '7'].freeze,
14
+ 'o' => ['0'].freeze,
15
+ 's' => ['$', '5'].freeze,
16
+ 't' => ['+', '7'].freeze,
17
+ 'x' => ['%'].freeze,
18
+ 'z' => ['2'].freeze
19
+ }.freeze
18
20
 
19
21
  def initialize(dictionary_matchers)
20
22
  @dictionary_matchers = dictionary_matchers
@@ -24,20 +26,20 @@ module Zxcvbn
24
26
  matches = []
25
27
  lowercased_password = password.downcase
26
28
  combinations_to_try = substitution_combinations(relevant_l33t_substitutions(lowercased_password))
27
- # debugger if password == 'abcdefghijk987654321'
28
- combinations_to_try.each do |substitution|
29
+ combinations_to_try.each do |substitutions|
29
30
  @dictionary_matchers.each do |matcher|
30
- subbed_password = substitute(lowercased_password, substitution)
31
+ subbed_password = substitute(lowercased_password, substitutions)
31
32
  matcher.matches(subbed_password).each do |match|
32
- token = lowercased_password[match.i..match.j]
33
+ length = match.j - match.i + 1
34
+ token = lowercased_password.slice(match.i, length)
33
35
  next if token == match.matched_word.downcase
34
- # debugger if token == '1'
36
+
35
37
  match_substitutions = {}
36
- substitution.each do |letter, substitution|
38
+ substitutions.each do |letter, substitution|
37
39
  match_substitutions[substitution] = letter if token.include?(substitution)
38
40
  end
39
41
  match.l33t = true
40
- match.token = password[match.i..match.j]
42
+ match.token = password.slice(match.i, length)
41
43
  match.sub = match_substitutions
42
44
  match.sub_display = match_substitutions.map do |k, v|
43
45
  "#{k} -> #{v}"
@@ -49,9 +51,9 @@ module Zxcvbn
49
51
  matches
50
52
  end
51
53
 
52
- def substitute(password, substitution)
54
+ def substitute(password, substitutions)
53
55
  subbed_password = password.dup
54
- substitution.each do |letter, substitution|
56
+ substitutions.each do |letter, substitution|
55
57
  subbed_password.gsub!(substitution, letter)
56
58
  end
57
59
  subbed_password
@@ -64,9 +66,7 @@ module Zxcvbn
64
66
  end
65
67
  L33T_TABLE.each do |letter, substibutions|
66
68
  password.each_char do |password_char|
67
- if substibutions.include?(password_char)
68
- subs[letter] << password_char
69
- end
69
+ subs[letter] << password_char if substibutions.include?(password_char)
70
70
  end
71
71
  end
72
72
  subs
@@ -82,7 +82,7 @@ module Zxcvbn
82
82
  expanded_substitutions.each do |substitution_hash|
83
83
  # convert a hash to an array of hashes with 1 key each
84
84
  subs_array = substitution_hash.map do |letter, substitutions|
85
- {letter => substitutions}
85
+ { letter => substitutions }
86
86
  end
87
87
  combinations << subs_array
88
88
 
@@ -94,15 +94,13 @@ module Zxcvbn
94
94
  end
95
95
 
96
96
  # convert back to simple hash per substitution combination
97
- combination_hashes = combinations.map do |combination_set|
97
+ combinations.map do |combination_set|
98
98
  hash = {}
99
99
  combination_set.each do |combination_hash|
100
100
  hash.merge!(combination_hash)
101
101
  end
102
102
  hash
103
103
  end
104
-
105
- combination_hashes
106
104
  end
107
105
 
108
106
  # expand possible combinations if multiple characters can be substituted
@@ -110,11 +108,11 @@ module Zxcvbn
110
108
  # [{'a' => '4', 'i' => 1}, {'a' => '@', 'i' => '1'}]
111
109
  def expanded_substitutions(hash)
112
110
  return {} if hash.empty?
111
+
113
112
  values = hash.values
114
113
  product_values = values[0].product(*values[1..-1])
115
- product_values.map{ |p| Hash[hash.keys.zip(p)] }
114
+ product_values.map { |p| Hash[hash.keys.zip(p)] }
116
115
  end
117
-
118
116
  end
119
117
  end
120
- end
118
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/match'
2
4
 
3
5
  module Zxcvbn
@@ -5,15 +7,15 @@ module Zxcvbn
5
7
  module RegexHelpers
6
8
  def re_match_all(regex, password)
7
9
  pos = 0
8
- while re_match = regex.match(password, pos)
10
+ while (re_match = regex.match(password, pos))
9
11
  i, j = re_match.offset(0)
10
12
  pos = j
11
13
  j -= 1
12
14
 
13
15
  match = Match.new(
14
- :i => i,
15
- :j => j,
16
- :token => password[i..j]
16
+ i: i,
17
+ j: j,
18
+ token: password.slice(i, j - i + 1)
17
19
  )
18
20
  yield match, re_match
19
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/match'
2
4
 
3
5
  module Zxcvbn
@@ -9,17 +11,15 @@ module Zxcvbn
9
11
  while i < password.length
10
12
  cur_char = password[i]
11
13
  j = i + 1
12
- while cur_char == password[j]
13
- j += 1
14
- end
14
+ j += 1 while cur_char == password[j]
15
15
 
16
16
  if j - i > 2 # don't consider length 1 or 2 chains.
17
17
  result << Match.new(
18
- :pattern => 'repeat',
19
- :i => i,
20
- :j => j-1,
21
- :token => password[i...j],
22
- :repeated_char => cur_char
18
+ pattern: 'repeat',
19
+ i: i,
20
+ j: j - 1,
21
+ token: password.slice(i, j - i),
22
+ repeated_char: cur_char
23
23
  )
24
24
  end
25
25
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/match'
2
4
 
3
5
  module Zxcvbn
@@ -7,14 +9,14 @@ module Zxcvbn
7
9
  'lower' => 'abcdefghijklmnopqrstuvwxyz',
8
10
  'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
9
11
  'digits' => '01234567890'
10
- }
12
+ }.freeze
11
13
 
12
14
  def seq_match_length(password, from, direction, seq)
13
15
  index_from = seq.index(password[from])
14
16
  j = 1
15
17
  while from + j < password.length &&
16
18
  password[from + j] == seq[index_from + direction * j]
17
- j+= 1
19
+ j += 1
18
20
  end
19
21
  j
20
22
  end
@@ -24,15 +26,13 @@ module Zxcvbn
24
26
  def applicable_sequence(password, i)
25
27
  SEQUENCES.each do |name, sequence|
26
28
  index1 = sequence.index(password[i])
27
- index2 = sequence.index(password[i+1])
28
- if index1 and index2
29
- seq_direction = index2 - index1
30
- if [-1, 1].include?(seq_direction)
31
- return [name, sequence, seq_direction]
32
- else
33
- return nil
34
- end
35
- end
29
+ index2 = sequence.index(password[i + 1])
30
+ next unless index1 && index2
31
+
32
+ seq_direction = index2 - index1
33
+ return [name, sequence, seq_direction] if [-1, 1].include?(seq_direction)
34
+
35
+ return nil
36
36
  end
37
37
  end
38
38
 
@@ -46,13 +46,13 @@ module Zxcvbn
46
46
  length = seq_match_length(password, i, seq_direction, seq)
47
47
  if length > 2
48
48
  result << Match.new(
49
- :pattern => 'sequence',
50
- :i => i,
51
- :j => i + length - 1,
52
- :token => password[i, length],
53
- :sequence_name => seq_name,
54
- :sequence_space => seq.length,
55
- :ascending => seq_direction == 1
49
+ pattern: 'sequence',
50
+ i: i,
51
+ j: i + length - 1,
52
+ token: password[i, length],
53
+ sequence_name: seq_name,
54
+ sequence_space: seq.length,
55
+ ascending: seq_direction == 1
56
56
  )
57
57
  end
58
58
  i += length - 1
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/match'
2
4
 
3
5
  module Zxcvbn
@@ -24,7 +26,7 @@ module Zxcvbn
24
26
  turns = 0
25
27
  shifted_count = 0
26
28
  loop do
27
- prev_char = password[j-1]
29
+ prev_char = password[j - 1]
28
30
  found = false
29
31
  found_direction = -1
30
32
  cur_direction = -1
@@ -34,22 +36,22 @@ module Zxcvbn
34
36
  cur_char = password[j]
35
37
  adjacents.each do |adj|
36
38
  cur_direction += 1
37
- if adj && adj.index(cur_char)
38
- found = true
39
- found_direction = cur_direction
40
- if adj.index(cur_char) == 1
41
- # index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
42
- # for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
43
- shifted_count += 1
44
- end
45
- if last_direction != found_direction
46
- # adding a turn is correct even in the initial case when last_direction is null:
47
- # every spatial pattern starts with a turn.
48
- turns += 1
49
- last_direction = found_direction
50
- end
51
- break
39
+ next unless adj&.index(cur_char)
40
+
41
+ found = true
42
+ found_direction = cur_direction
43
+ if adj.index(cur_char) == 1
44
+ # index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
45
+ # for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
46
+ shifted_count += 1
47
+ end
48
+ if last_direction != found_direction
49
+ # adding a turn is correct even in the initial case when last_direction is null:
50
+ # every spatial pattern starts with a turn.
51
+ turns += 1
52
+ last_direction = found_direction
52
53
  end
54
+ break
53
55
  end
54
56
  end
55
57
  # if the current pattern continued, extend j and try to grow again
@@ -59,13 +61,13 @@ module Zxcvbn
59
61
  # otherwise push the pattern discovered so far, if any...
60
62
  if j - i > 2 # don't consider length 1 or 2 chains.
61
63
  result << Match.new(
62
- :pattern => 'spatial',
63
- :i => i,
64
- :j => j-1,
65
- :token => password[i...j],
66
- :graph => graph_name,
67
- :turns => turns,
68
- :shifted_count => shifted_count
64
+ pattern: 'spatial',
65
+ i: i,
66
+ j: j - 1,
67
+ token: password.slice(i, j - i),
68
+ graph: graph_name,
69
+ turns: turns,
70
+ shifted_count: shifted_count
69
71
  )
70
72
  end
71
73
  # ...and then start a new search for the rest of the password.
@@ -78,4 +80,4 @@ module Zxcvbn
78
80
  end
79
81
  end
80
82
  end
81
- end
83
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/matchers/regex_helpers'
2
4
 
3
5
  module Zxcvbn
@@ -5,7 +7,7 @@ module Zxcvbn
5
7
  class Year
6
8
  include RegexHelpers
7
9
 
8
- YEAR_REGEX = /19\d\d|200\d|201\d/
10
+ YEAR_REGEX = /19\d\d|200\d|201\d/.freeze
9
11
 
10
12
  def matches(password)
11
13
  result = []
@@ -17,4 +19,4 @@ module Zxcvbn
17
19
  end
18
20
  end
19
21
  end
20
- end
22
+ end
data/lib/zxcvbn/math.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zxcvbn
2
4
  module Math
3
5
  def bruteforce_cardinality(password)
@@ -30,25 +32,27 @@ module Zxcvbn
30
32
 
31
33
  def nCk(n, k)
32
34
  return 0 if k > n
33
- return 1 if k == 0
35
+ return 1 if k.zero?
36
+
37
+ # Use symmetry property: C(n,k) = C(n, n-k)
38
+ # Choose smaller k to minimize iterations
39
+ k = n - k if k > n - k
40
+
34
41
  r = 1
35
42
  (1..k).each do |d|
36
- r = r * n
37
- r = r / d
43
+ r *= n
44
+ r /= d
38
45
  n -= 1
39
46
  end
40
47
  r
41
48
  end
42
49
 
43
50
  def average_degree_for_graph(graph_name)
44
- graph = data.adjacency_graphs[graph_name]
45
- degrees = graph.map { |_, neighbors| neighbors.compact.size }
46
- sum = degrees.inject(0, :+)
47
- sum.to_f / graph.size
51
+ data.graph_stats[graph_name][:average_degree]
48
52
  end
49
53
 
50
54
  def starting_positions_for_graph(graph_name)
51
- data.adjacency_graphs[graph_name].length
55
+ data.graph_stats[graph_name][:starting_positions]
52
56
  end
53
57
  end
54
58
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/dictionary_ranker'
2
4
  require 'zxcvbn/matchers/dictionary'
3
5
  require 'zxcvbn/matchers/l33t'
@@ -26,17 +28,18 @@ module Zxcvbn
26
28
 
27
29
  def user_input_matchers(user_inputs)
28
30
  return [] unless user_inputs.any?
31
+
29
32
  user_ranked_dictionary = DictionaryRanker.rank_dictionary(user_inputs)
30
33
  dictionary_matcher = Matchers::Dictionary.new('user_inputs', user_ranked_dictionary)
31
34
  l33t_matcher = Matchers::L33t.new([dictionary_matcher])
32
35
  [dictionary_matcher, l33t_matcher]
33
36
  end
34
37
 
35
-
36
38
  def build_matchers
37
39
  matchers = []
38
40
  dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary|
39
- Matchers::Dictionary.new(name, dictionary)
41
+ trie = @data.dictionary_tries[name]
42
+ Matchers::Dictionary.new(name, dictionary, trie)
40
43
  end
41
44
  l33t_matcher = Matchers::L33t.new(dictionary_matchers)
42
45
  matchers += dictionary_matchers