zxcvbn 0.1.8 → 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: 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: ''