zxcvbn 0.1.9 → 0.1.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a374558176fa132c830032ab3cf333483389b98ce54ef92bfd026400362a224
4
- data.tar.gz: eb751fc63e94b4573144f66ef9cc343695104cd4461fd170931b5b5fa06e0ad2
3
+ metadata.gz: 2c519c7ba0712720763e56d09f829f3e23a1c7deb0684380da425613185d1916
4
+ data.tar.gz: dbd22f8b2d540d61e78db3e0be2eab4637dddb4b8bc585be3c4e78a64b56ed05
5
5
  SHA512:
6
- metadata.gz: e50119ccf438121beee719afe200c7a2085c0a266f5c5595fb95bebc01a10fca980dec4df8a30374dc5f7cf7bb5c57708a7f71565877ee8885d6740e54e71d66
7
- data.tar.gz: 1323dff6d9433298bc44c4a2632de42af7137629d02113e60f2a64f6d184967a27a13e161ff3d2404b6a58177d880465bd865cc4d95158bd33c07849ed0361f4
6
+ metadata.gz: 5e75faed712c76af15e06520f2606d94b9b65c319d02e5833a89d669b771f74061f986c88ad0ddcb8c78dca5f0298ae681155bad24f249df7c257f0bf3a18c27
7
+ data.tar.gz: cfad539afdfd6bf16a0d541a87cbbbf56f77212e4b89d8e676ead50d97b6058e2bcac2081092095594795823971149330ca49cd5aae94b43f4efff1b0e63adce
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.1.10] - 2023-10-15
2
+ - [#10] Refactor implementation to avoid thread safety issues for user inputs
3
+
4
+ *Adam Kiczula (@adamkiczula)*
5
+
1
6
  ## [0.1.9] - 2023-01-27
2
7
  - [#6] [#7] Security/Performance fix to vulnerability to DoS attacks.
3
8
 
data/README.md CHANGED
@@ -95,7 +95,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
95
95
 
96
96
  ## Contributing
97
97
 
98
- Bug reports and pull requests are welcome on GitHub at https://github.com/formigarafa/zxcvbn. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/zxcvbn/blob/master/CODE_OF_CONDUCT.md).
98
+ Bug reports and pull requests are welcome on GitHub at https://github.com/formigarafa/zxcvbn-rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/formigarafa/zxcvbn-rb/blob/master/CODE_OF_CONDUCT.md).
99
99
 
100
100
  ## License
101
101
 
@@ -103,4 +103,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
103
103
 
104
104
  ## Code of Conduct
105
105
 
106
- Everyone interacting in the Zxcvbn project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/zxcvbn/blob/master/CODE_OF_CONDUCT.md).
106
+ Everyone interacting in the Zxcvbn project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/formigarafa/zxcvbn-rb/blob/master/CODE_OF_CONDUCT.md).
@@ -128,64 +128,65 @@ module Zxcvbn
128
128
  # ------------------------------------------------------------------------------
129
129
  # omnimatch -- combine everything ----------------------------------------------
130
130
  # ------------------------------------------------------------------------------
131
- def self.omnimatch(password)
131
+ def self.omnimatch(password, user_inputs = [])
132
+ user_dict = build_user_input_dictionary(user_inputs)
132
133
  matches = []
133
- matchers = [
134
- :dictionary_match,
135
- :reverse_dictionary_match,
136
- :l33t_match,
137
- :spatial_match,
138
- :repeat_match,
139
- :sequence_match,
140
- :regex_match,
141
- :date_match
142
- ]
143
- matchers.each do |matcher|
144
- matches += send(matcher, password)
145
- end
134
+ matches += dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES)
135
+ matches += reverse_dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES)
136
+ matches += l33t_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES, _l33t_table = L33T_TABLE)
137
+ matches += spatial_match(password, _graphs = GRAPHS)
138
+ matches += repeat_match(password, user_dict)
139
+ matches += sequence_match(password)
140
+ matches += regex_match(password, _regexen = REGEXEN)
141
+ matches += date_match(password)
146
142
  sorted(matches)
147
143
  end
148
144
 
149
145
  #-------------------------------------------------------------------------------
150
146
  # dictionary match (common passwords, english, last names, etc) ----------------
151
147
  #-------------------------------------------------------------------------------
152
- def self.dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES)
148
+ def self.dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES)
153
149
  # _ranked_dictionaries variable is for unit testing purposes
154
150
  matches = []
151
+ _ranked_dictionaries.each do |dictionary_name, ranked_dict|
152
+ check_dictionary(matches, password, dictionary_name, ranked_dict)
153
+ end
154
+ check_dictionary(matches, password, "user_inputs", user_dict)
155
+ sorted(matches)
156
+ end
157
+
158
+ def self.check_dictionary(matches, password, dictionary_name, ranked_dict)
155
159
  len = password.length
156
160
  password_lower = password.downcase
157
- _ranked_dictionaries.each do |dictionary_name, ranked_dict|
158
- longest_dict_word_size = RANKED_DICTIONARIES_MAX_WORD_SIZE.fetch(dictionary_name) do
159
- ranked_dict.keys.max_by(&:size)&.size || 0
160
- end
161
- search_width = [longest_dict_word_size, len].min
162
- (0...len).each do |i|
163
- search_end = [i + search_width, len].min
164
- (i...search_end).each do |j|
165
- if ranked_dict.key?(password_lower[i..j])
166
- word = password_lower[i..j]
167
- rank = ranked_dict[word]
168
- matches << {
169
- "pattern" => "dictionary",
170
- "i" => i,
171
- "j" => j,
172
- "token" => password[i..j],
173
- "matched_word" => word,
174
- "rank" => rank,
175
- "dictionary_name" => dictionary_name,
176
- "reversed" => false,
177
- "l33t" => false
178
- }
179
- end
161
+ longest_word_size = RANKED_DICTIONARIES_MAX_WORD_SIZE.fetch(dictionary_name) do
162
+ ranked_dict.keys.max_by(&:size)&.size || 0
163
+ end
164
+ search_width = [longest_word_size, len].min
165
+ (0...len).each do |i|
166
+ search_end = [i + search_width, len].min
167
+ (i...search_end).each do |j|
168
+ if ranked_dict.key?(password_lower[i..j])
169
+ word = password_lower[i..j]
170
+ rank = ranked_dict[word]
171
+ matches << {
172
+ "pattern" => "dictionary",
173
+ "i" => i,
174
+ "j" => j,
175
+ "token" => password[i..j],
176
+ "matched_word" => word,
177
+ "rank" => rank,
178
+ "dictionary_name" => dictionary_name,
179
+ "reversed" => false,
180
+ "l33t" => false
181
+ }
180
182
  end
181
183
  end
182
184
  end
183
- sorted(matches)
184
185
  end
185
186
 
186
- def self.reverse_dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES)
187
+ def self.reverse_dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES)
187
188
  reversed_password = password.reverse
188
- matches = dictionary_match(reversed_password, _ranked_dictionaries)
189
+ matches = dictionary_match(reversed_password, user_dict, _ranked_dictionaries)
189
190
  matches.each do |match|
190
191
  match["token"] = match["token"].reverse
191
192
  match["reversed"] = true
@@ -195,10 +196,15 @@ module Zxcvbn
195
196
  sorted(matches)
196
197
  end
197
198
 
198
- def self.user_input_dictionary=(ordered_list)
199
- ranked_dict = build_ranked_dict(ordered_list.dup)
200
- RANKED_DICTIONARIES["user_inputs"] = ranked_dict
201
- RANKED_DICTIONARIES_MAX_WORD_SIZE["user_inputs"] = ranked_dict.keys.max_by(&:size)&.size || 0
199
+ def self.build_user_input_dictionary(user_inputs_or_dict)
200
+ # optimization: if we receive a hash, we've been given the dict back (from the repeat matcher)
201
+ return user_inputs_or_dict if user_inputs_or_dict.is_a?(Hash)
202
+
203
+ sanitized_inputs = []
204
+ user_inputs_or_dict.each do |arg|
205
+ sanitized_inputs << arg.to_s.downcase if arg.is_a?(String) || arg.is_a?(Numeric) || arg == true || arg == false
206
+ end
207
+ build_ranked_dict(sanitized_inputs)
202
208
  end
203
209
 
204
210
  #-------------------------------------------------------------------------------
@@ -287,13 +293,13 @@ module Zxcvbn
287
293
  sub_dicts
288
294
  end
289
295
 
290
- def self.l33t_match(password, _ranked_dictionaries = RANKED_DICTIONARIES, _l33t_table = L33T_TABLE)
296
+ def self.l33t_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES, _l33t_table = L33T_TABLE)
291
297
  matches = []
292
298
  enumerate_l33t_subs(relevant_l33t_subtable(password, _l33t_table)).each do |sub|
293
299
  break if sub.empty? # corner case: password has no relevant subs.
294
300
 
295
301
  subbed_password = translate(password, sub)
296
- dictionary_match(subbed_password, _ranked_dictionaries).each do |match|
302
+ dictionary_match(subbed_password, user_dict, _ranked_dictionaries).each do |match|
297
303
  token = password[match["i"]..match["j"]]
298
304
  if token.downcase == match["matched_word"]
299
305
  next # only return the matches that contain an actual substitution
@@ -403,7 +409,7 @@ module Zxcvbn
403
409
  #-------------------------------------------------------------------------------
404
410
  # repeats (aaa, abcabcabc) and sequences (abcdef) ------------------------------
405
411
  #-------------------------------------------------------------------------------
406
- def self.repeat_match(password)
412
+ def self.repeat_match(password, user_dict)
407
413
  matches = []
408
414
  greedy = /(.+)\1+/
409
415
  lazy = /(.+?)\1+/
@@ -436,7 +442,7 @@ module Zxcvbn
436
442
  i = match.begin(0)
437
443
  j = match.end(0) - 1
438
444
  # recursively match and score the base string
439
- base_analysis = Scoring.most_guessable_match_sequence(base_token, omnimatch(base_token))
445
+ base_analysis = Scoring.most_guessable_match_sequence(base_token, omnimatch(base_token, user_dict))
440
446
  base_matches = base_analysis["sequence"]
441
447
  base_guesses = base_analysis["guesses"]
442
448
  matches << {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zxcvbn
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.10"
5
5
  end
data/lib/zxcvbn.rb CHANGED
@@ -13,13 +13,7 @@ module Zxcvbn
13
13
 
14
14
  def self.zxcvbn(password, user_inputs = [])
15
15
  start = (Time.now.to_f * 1000).to_i
16
- # reset the user inputs matcher on a per-request basis to keep things stateless
17
- sanitized_inputs = []
18
- user_inputs.each do |arg|
19
- sanitized_inputs << arg.to_s.downcase if arg.is_a?(String) || arg.is_a?(Numeric) || arg == true || arg == false
20
- end
21
- Matching.user_input_dictionary = sanitized_inputs
22
- matches = Matching.omnimatch(password)
16
+ matches = Matching.omnimatch(password, user_inputs)
23
17
  result = Scoring.most_guessable_match_sequence(password, matches)
24
18
  result["calc_time"] = (Time.now.to_f * 1000).to_i - start
25
19
  attack_times = TimeEstimates.estimate_attack_times(result["guesses"])
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.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Santos
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-27 00:00:00.000000000 Z
11
+ date: 2023-10-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 100% native Ruby 100% compatible port of Dropbox's zxcvbn.js
14
14
  email:
@@ -52,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
54
  requirements: []
55
- rubygems_version: 3.0.3.1
55
+ rubygems_version: 3.4.10
56
56
  signing_key:
57
57
  specification_version: 4
58
58
  summary: ''