zxcvbn 0.1.9 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: ''