zxcvbn 0.1.9 → 0.1.10

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