zxcvbn 0.1.8 → 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: fa46dde2a5eb2757753576eb8ff1fb74d7bdb3012dc91c6c9cfd7bc70ca91675
4
- data.tar.gz: 3cf3b2e04f76138324548ea35ac3291833cb159b520b207a846cdab5e1a50e7b
3
+ metadata.gz: 2c519c7ba0712720763e56d09f829f3e23a1c7deb0684380da425613185d1916
4
+ data.tar.gz: dbd22f8b2d540d61e78db3e0be2eab4637dddb4b8bc585be3c4e78a64b56ed05
5
5
  SHA512:
6
- metadata.gz: 06a251cda230ac1992543b624d64f8b3b8d33bdf9f579deb9bce8d94da65de3f4625330d14c98aa1587d3ccce08cd2998e3f9302c1cfcf9acff6ecc5883fe8d0
7
- data.tar.gz: 884c32486ad5332b429939fa8180c49fb1d381b36d4ae0b69518d223642a2fa48af595bf8efe11568ff416e44a555ac5ee4854349b25623876bb61b02112b78a
6
+ metadata.gz: 5e75faed712c76af15e06520f2606d94b9b65c319d02e5833a89d669b771f74061f986c88ad0ddcb8c78dca5f0298ae681155bad24f249df7c257f0bf3a18c27
7
+ data.tar.gz: cfad539afdfd6bf16a0d541a87cbbbf56f77212e4b89d8e676ead50d97b6058e2bcac2081092095594795823971149330ca49cd5aae94b43f4efff1b0e63adce
data/CHANGELOG.md CHANGED
@@ -1,11 +1,19 @@
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
+
6
+ ## [0.1.9] - 2023-01-27
7
+ - [#6] [#7] Security/Performance fix to vulnerability to DoS attacks.
8
+
1
9
  ## [0.1.8] - 2023-01-22
2
10
  - How to find information on translations on README.
3
11
  - Drop automatic tests on ruby 2.5 (It still works on it but development gems are failing to build).
4
12
  - Update dev gems to prepare to test on Ruby 3.1 and 3.2. (mini_racer, rubocop and bundler)
5
- - Fix Style/RedundantStringEscape on frequency_lists.rb
6
- - Add automated tests for Ruby 3.1 and 3.2
7
- - Add MFA requirement on release
8
- - Trim non-production files from final gem
13
+ - Fix Style/RedundantStringEscape on frequency_lists.rb.
14
+ - Add automated tests for Ruby 3.1 and 3.2.
15
+ - Add MFA requirement on release.
16
+ - Trim non-production files from final gem.
9
17
 
10
18
  ## [0.1.7] - 2021-06-12
11
19
  - Ported original specs
data/README.md CHANGED
@@ -5,7 +5,20 @@
5
5
 
6
6
  Ruby port of Dropbox's [zxcvbn.js](https://github.com/dropbox/zxcvbn) JavaScript library running completely in Ruby (no need to load execjs or libv8).
7
7
 
8
- The intention is to provide an option 100% Ruby solution with all the same features and same results (or as close to the original JS function as possible).
8
+ ### Goals:
9
+ - Exact same results as [dropbox/zxcvbn.js (Version 4.4.2)](https://github.com/dropbox/zxcvbn). If **result compatibility** is found or made different a major version will be bumped so no one is caught off guard.
10
+ - Parity of features to [dropbox/zxcvbn.js (Version 4.4.2)](https://github.com/dropbox/zxcvbn) interface.
11
+ - 100% native Ruby solution: **No Javascript Runtime**.
12
+
13
+ ### Compatible with [zxcvbn-js](https://github.com/bitzesty/zxcvbn-js) and [zxcvbn-ruby](https://github.com/envato/zxcvbn-ruby)
14
+
15
+ This gem include compatibility interfaces so it can be used as a drop-in substitution both of the most popular alternatives `zxcvbn-js` and `zxcvbn-ruby`). Besides `Zxcvbn.zxcvbn` you can just call `Zxcvbn.test` or use `Zxcvbn::Tester.new` the same way as you would if you were using any of them.
16
+
17
+ | | `zxcvbn-rb` | `zxcvbn-js` | `zxcvbn-ruby` |
18
+ |------------------------------------|------------------------|------------------------|------------------------|
19
+ | Results match `zxcvbn.js (V4.4.2)` | :white_check_mark: yes | :white_check_mark: yes | :x: no |
20
+ | Run without Javascript Runtime | :white_check_mark: yes | :x: no | :white_check_mark: yes |
21
+ | Interface compatibility with others| :white_check_mark: yes | :x: no | :x: no |
9
22
 
10
23
  ## Installation
11
24
 
@@ -71,10 +84,6 @@ Zxcvbn.zxcvbn("password")
71
84
  }
72
85
  ```
73
86
 
74
- ### Compatible with `zxcvbn-js` and `zxcvbn-ruby`
75
-
76
- This gem include a compatible interface so it can be used as a drop-in substitution for `zxcvbn-js` or `zxcvbn-ruby`. You can just call `Zxcvbn.test` or use `Zxcvbn::Tester.new` the same way as you would if you were using `zxcvbn-js` or `zxcvbn-ruby`.
77
-
78
87
  ### Note about translations (i18n, gettext, etc...)
79
88
  Check the [wiki](https://github.com/formigarafa/zxcvbn-rb/wiki) for more details on how to handle translations.
80
89
 
@@ -86,7 +95,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
86
95
 
87
96
  ## Contributing
88
97
 
89
- 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).
90
99
 
91
100
  ## License
92
101
 
@@ -94,4 +103,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
94
103
 
95
104
  ## Code of Conduct
96
105
 
97
- 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).
@@ -15,6 +15,10 @@ module Zxcvbn
15
15
  build_ranked_dict(lst)
16
16
  end
17
17
 
18
+ RANKED_DICTIONARIES_MAX_WORD_SIZE = RANKED_DICTIONARIES.transform_values do |word_scores|
19
+ word_scores.keys.max_by(&:size).size
20
+ end
21
+
18
22
  GRAPHS = {
19
23
  "qwerty" => ADJACENCY_GRAPHS["qwerty"],
20
24
  "dvorak" => ADJACENCY_GRAPHS["dvorak"],
@@ -124,59 +128,65 @@ module Zxcvbn
124
128
  # ------------------------------------------------------------------------------
125
129
  # omnimatch -- combine everything ----------------------------------------------
126
130
  # ------------------------------------------------------------------------------
127
- def self.omnimatch(password)
131
+ def self.omnimatch(password, user_inputs = [])
132
+ user_dict = build_user_input_dictionary(user_inputs)
128
133
  matches = []
129
- matchers = [
130
- :dictionary_match,
131
- :reverse_dictionary_match,
132
- :l33t_match,
133
- :spatial_match,
134
- :repeat_match,
135
- :sequence_match,
136
- :regex_match,
137
- :date_match
138
- ]
139
- matchers.each do |matcher|
140
- matches += send(matcher, password)
141
- 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)
142
142
  sorted(matches)
143
143
  end
144
144
 
145
145
  #-------------------------------------------------------------------------------
146
146
  # dictionary match (common passwords, english, last names, etc) ----------------
147
147
  #-------------------------------------------------------------------------------
148
- def self.dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES)
148
+ def self.dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES)
149
149
  # _ranked_dictionaries variable is for unit testing purposes
150
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)
151
159
  len = password.length
152
160
  password_lower = password.downcase
153
- _ranked_dictionaries.each do |dictionary_name, ranked_dict|
154
- (0...len).each do |i|
155
- (i...len).each do |j|
156
- if ranked_dict.key?(password_lower[i..j])
157
- word = password_lower[i..j]
158
- rank = ranked_dict[word]
159
- matches << {
160
- "pattern" => "dictionary",
161
- "i" => i,
162
- "j" => j,
163
- "token" => password[i..j],
164
- "matched_word" => word,
165
- "rank" => rank,
166
- "dictionary_name" => dictionary_name,
167
- "reversed" => false,
168
- "l33t" => false
169
- }
170
- 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
+ }
171
182
  end
172
183
  end
173
184
  end
174
- sorted(matches)
175
185
  end
176
186
 
177
- def self.reverse_dictionary_match(password, _ranked_dictionaries = RANKED_DICTIONARIES)
187
+ def self.reverse_dictionary_match(password, user_dict, _ranked_dictionaries = RANKED_DICTIONARIES)
178
188
  reversed_password = password.reverse
179
- matches = dictionary_match(reversed_password, _ranked_dictionaries)
189
+ matches = dictionary_match(reversed_password, user_dict, _ranked_dictionaries)
180
190
  matches.each do |match|
181
191
  match["token"] = match["token"].reverse
182
192
  match["reversed"] = true
@@ -186,8 +196,15 @@ module Zxcvbn
186
196
  sorted(matches)
187
197
  end
188
198
 
189
- def self.user_input_dictionary=(ordered_list)
190
- RANKED_DICTIONARIES["user_inputs"] = build_ranked_dict(ordered_list.dup)
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)
191
208
  end
192
209
 
193
210
  #-------------------------------------------------------------------------------
@@ -276,13 +293,13 @@ module Zxcvbn
276
293
  sub_dicts
277
294
  end
278
295
 
279
- 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)
280
297
  matches = []
281
298
  enumerate_l33t_subs(relevant_l33t_subtable(password, _l33t_table)).each do |sub|
282
299
  break if sub.empty? # corner case: password has no relevant subs.
283
300
 
284
301
  subbed_password = translate(password, sub)
285
- dictionary_match(subbed_password, _ranked_dictionaries).each do |match|
302
+ dictionary_match(subbed_password, user_dict, _ranked_dictionaries).each do |match|
286
303
  token = password[match["i"]..match["j"]]
287
304
  if token.downcase == match["matched_word"]
288
305
  next # only return the matches that contain an actual substitution
@@ -392,7 +409,7 @@ module Zxcvbn
392
409
  #-------------------------------------------------------------------------------
393
410
  # repeats (aaa, abcabcabc) and sequences (abcdef) ------------------------------
394
411
  #-------------------------------------------------------------------------------
395
- def self.repeat_match(password)
412
+ def self.repeat_match(password, user_dict)
396
413
  matches = []
397
414
  greedy = /(.+)\1+/
398
415
  lazy = /(.+?)\1+/
@@ -425,7 +442,7 @@ module Zxcvbn
425
442
  i = match.begin(0)
426
443
  j = match.end(0) - 1
427
444
  # recursively match and score the base string
428
- 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))
429
446
  base_matches = base_analysis["sequence"]
430
447
  base_guesses = base_analysis["guesses"]
431
448
  matches << {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zxcvbn
4
- VERSION = "0.1.8"
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.8
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-22 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: ''