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 +4 -4
- data/CHANGELOG.md +12 -4
- data/README.md +16 -7
- data/lib/zxcvbn/matching.rb +59 -42
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +1 -7
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c519c7ba0712720763e56d09f829f3e23a1c7deb0684380da425613185d1916
|
4
|
+
data.tar.gz: dbd22f8b2d540d61e78db3e0be2eab4637dddb4b8bc585be3c4e78a64b56ed05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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/
|
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/
|
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).
|
data/lib/zxcvbn/matching.rb
CHANGED
@@ -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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
154
|
-
(
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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.
|
190
|
-
|
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 << {
|
data/lib/zxcvbn/version.rb
CHANGED
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
|
-
|
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.
|
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-
|
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.
|
55
|
+
rubygems_version: 3.4.10
|
56
56
|
signing_key:
|
57
57
|
specification_version: 4
|
58
58
|
summary: ''
|