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.
@@ -11,7 +11,7 @@ module Zxcvbn
11
11
  result
12
12
  end
13
13
 
14
- RANKED_DICTIONARIES = FREQUENCY_LISTS.transform_values do |lst|
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
- 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 << {
@@ -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.each do |_key, neighbors|
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
@@ -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.12"
5
5
  end
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
- # 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)
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
- OpenStruct.new(Zxcvbn.zxcvbn(password, user_inputs)) # rubocop:disable Style/OpenStructUse
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.9
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: 2023-01-27 00:00:00.000000000 Z
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.0.3.1
61
+ rubygems_version: 3.5.11
56
62
  signing_key:
57
63
  specification_version: 4
58
64
  summary: ''