zxcvbn 0.1.9 → 0.1.12
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 +16 -0
- data/README.md +2 -2
- data/lib/zxcvbn/frequency_lists/english_wikipedia.txt +30000 -0
- data/lib/zxcvbn/frequency_lists/female_names.txt +3712 -0
- data/lib/zxcvbn/frequency_lists/male_names.txt +983 -0
- data/lib/zxcvbn/frequency_lists/passwords.txt +30000 -0
- data/lib/zxcvbn/frequency_lists/surnames.txt +10000 -0
- data/lib/zxcvbn/frequency_lists/us_tv_and_film.txt +19160 -0
- data/lib/zxcvbn/frequency_lists.rb +17 -9
- data/lib/zxcvbn/matching.rb +56 -50
- data/lib/zxcvbn/scoring.rb +1 -1
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +14 -8
- metadata +9 -3
data/lib/zxcvbn/matching.rb
CHANGED
@@ -11,7 +11,7 @@ module Zxcvbn
|
|
11
11
|
result
|
12
12
|
end
|
13
13
|
|
14
|
-
RANKED_DICTIONARIES =
|
14
|
+
RANKED_DICTIONARIES = Zxcvbn.frequency_lists.transform_values do |lst|
|
15
15
|
build_ranked_dict(lst)
|
16
16
|
end
|
17
17
|
|
@@ -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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
(i
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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.
|
199
|
-
|
200
|
-
|
201
|
-
|
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 << {
|
data/lib/zxcvbn/scoring.rb
CHANGED
@@ -6,7 +6,7 @@ module Zxcvbn
|
|
6
6
|
# this calculates the average over all keys.
|
7
7
|
def self.calc_average_degree(graph)
|
8
8
|
average = 0
|
9
|
-
graph.
|
9
|
+
graph.each_value do |neighbors|
|
10
10
|
average += neighbors.count { |n| n }.to_f
|
11
11
|
end
|
12
12
|
average /= graph.keys.size.to_f
|
data/lib/zxcvbn/version.rb
CHANGED
data/lib/zxcvbn.rb
CHANGED
@@ -10,16 +10,22 @@ require_relative "zxcvbn/version"
|
|
10
10
|
|
11
11
|
module Zxcvbn
|
12
12
|
class Error < StandardError; end
|
13
|
+
Result = Struct.new(
|
14
|
+
:password,
|
15
|
+
:guesses,
|
16
|
+
:guesses_log10,
|
17
|
+
:sequence,
|
18
|
+
:calc_time,
|
19
|
+
:crack_times_seconds,
|
20
|
+
:crack_times_display,
|
21
|
+
:score,
|
22
|
+
:feedback,
|
23
|
+
keyword_init: true
|
24
|
+
)
|
13
25
|
|
14
26
|
def self.zxcvbn(password, user_inputs = [])
|
15
27
|
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)
|
28
|
+
matches = Matching.omnimatch(password, user_inputs)
|
23
29
|
result = Scoring.most_guessable_match_sequence(password, matches)
|
24
30
|
result["calc_time"] = (Time.now.to_f * 1000).to_i - start
|
25
31
|
attack_times = TimeEstimates.estimate_attack_times(result["guesses"])
|
@@ -31,7 +37,7 @@ module Zxcvbn
|
|
31
37
|
end
|
32
38
|
|
33
39
|
def self.test(password, user_inputs = [])
|
34
|
-
|
40
|
+
Result.new(Zxcvbn.zxcvbn(password, user_inputs))
|
35
41
|
end
|
36
42
|
|
37
43
|
class Tester
|
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.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rafael Santos
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-10-29 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:
|
@@ -24,6 +24,12 @@ files:
|
|
24
24
|
- lib/zxcvbn/adjacency_graphs.rb
|
25
25
|
- lib/zxcvbn/feedback.rb
|
26
26
|
- lib/zxcvbn/frequency_lists.rb
|
27
|
+
- lib/zxcvbn/frequency_lists/english_wikipedia.txt
|
28
|
+
- lib/zxcvbn/frequency_lists/female_names.txt
|
29
|
+
- lib/zxcvbn/frequency_lists/male_names.txt
|
30
|
+
- lib/zxcvbn/frequency_lists/passwords.txt
|
31
|
+
- lib/zxcvbn/frequency_lists/surnames.txt
|
32
|
+
- lib/zxcvbn/frequency_lists/us_tv_and_film.txt
|
27
33
|
- lib/zxcvbn/matching.rb
|
28
34
|
- lib/zxcvbn/scoring.rb
|
29
35
|
- lib/zxcvbn/time_estimates.rb
|
@@ -52,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
58
|
- !ruby/object:Gem::Version
|
53
59
|
version: '0'
|
54
60
|
requirements: []
|
55
|
-
rubygems_version: 3.
|
61
|
+
rubygems_version: 3.5.11
|
56
62
|
signing_key:
|
57
63
|
specification_version: 4
|
58
64
|
summary: ''
|