zxcvbn-ruby 1.2.0 → 1.4.0
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 +72 -2
- data/README.md +3 -12
- data/lib/zxcvbn/clock.rb +11 -0
- data/lib/zxcvbn/crack_time.rb +14 -14
- data/lib/zxcvbn/data.rb +42 -9
- data/lib/zxcvbn/dictionary_ranker.rb +7 -6
- data/lib/zxcvbn/entropy.rb +52 -48
- data/lib/zxcvbn/feedback.rb +2 -0
- data/lib/zxcvbn/feedback_giver.rb +4 -3
- data/lib/zxcvbn/match.rb +18 -4
- data/lib/zxcvbn/matchers/date.rb +26 -21
- data/lib/zxcvbn/matchers/dictionary.rb +48 -13
- data/lib/zxcvbn/matchers/digits.rb +3 -1
- data/lib/zxcvbn/matchers/l33t.rb +56 -38
- data/lib/zxcvbn/matchers/new_l33t.rb +30 -32
- data/lib/zxcvbn/matchers/regex_helpers.rb +6 -4
- data/lib/zxcvbn/matchers/repeat.rb +8 -8
- data/lib/zxcvbn/matchers/sequences.rb +18 -18
- data/lib/zxcvbn/matchers/spatial.rb +26 -24
- data/lib/zxcvbn/matchers/year.rb +4 -2
- data/lib/zxcvbn/math.rb +12 -8
- data/lib/zxcvbn/omnimatch.rb +5 -2
- data/lib/zxcvbn/password_strength.rb +6 -4
- data/lib/zxcvbn/score.rb +3 -1
- data/lib/zxcvbn/scorer.rb +26 -23
- data/lib/zxcvbn/tester.rb +2 -2
- data/lib/zxcvbn/trie.rb +44 -0
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +4 -2
- data/sig/README.md +65 -0
- data/sig/zxcvbn/crack_time.rbs +13 -0
- data/sig/zxcvbn/data.rbs +31 -0
- data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
- data/sig/zxcvbn/entropy.rbs +33 -0
- data/sig/zxcvbn/feedback.rbs +8 -0
- data/sig/zxcvbn/feedback_giver.rbs +13 -0
- data/sig/zxcvbn/match.rbs +38 -0
- data/sig/zxcvbn/math.rbs +13 -0
- data/sig/zxcvbn/omnimatch.rbs +16 -0
- data/sig/zxcvbn/password_strength.rbs +10 -0
- data/sig/zxcvbn/score.rbs +15 -0
- data/sig/zxcvbn/scorer.rbs +20 -0
- data/sig/zxcvbn/tester.rbs +17 -0
- data/sig/zxcvbn/trie.rbs +13 -0
- data/sig/zxcvbn.rbs +10 -0
- metadata +27 -105
- data/.github/workflows/ci.yml +0 -23
- data/.gitignore +0 -18
- data/.rspec +0 -1
- data/CODE_OF_CONDUCT.md +0 -130
- data/Gemfile +0 -10
- data/Guardfile +0 -26
- data/Rakefile +0 -22
- data/spec/dictionary_ranker_spec.rb +0 -12
- data/spec/feedback_giver_spec.rb +0 -212
- data/spec/matchers/date_spec.rb +0 -109
- data/spec/matchers/dictionary_spec.rb +0 -30
- data/spec/matchers/digits_spec.rb +0 -15
- data/spec/matchers/l33t_spec.rb +0 -87
- data/spec/matchers/repeat_spec.rb +0 -18
- data/spec/matchers/sequences_spec.rb +0 -21
- data/spec/matchers/spatial_spec.rb +0 -20
- data/spec/matchers/year_spec.rb +0 -15
- data/spec/omnimatch_spec.rb +0 -24
- data/spec/scorer_spec.rb +0 -5
- data/spec/scoring/crack_time_spec.rb +0 -106
- data/spec/scoring/entropy_spec.rb +0 -216
- data/spec/scoring/math_spec.rb +0 -135
- data/spec/spec_helper.rb +0 -54
- data/spec/support/js_helpers.rb +0 -34
- data/spec/support/js_source/adjacency_graphs.js +0 -8
- data/spec/support/js_source/compiled.js +0 -1188
- data/spec/support/js_source/frequency_lists.js +0 -10
- data/spec/support/js_source/init.coffee +0 -63
- data/spec/support/js_source/init.js +0 -95
- data/spec/support/js_source/matching.coffee +0 -444
- data/spec/support/js_source/matching.js +0 -685
- data/spec/support/js_source/scoring.coffee +0 -270
- data/spec/support/js_source/scoring.js +0 -390
- data/spec/support/matcher.rb +0 -35
- data/spec/tester_spec.rb +0 -99
- data/spec/zxcvbn_spec.rb +0 -24
- data/zxcvbn-ruby.gemspec +0 -33
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -6,31 +8,64 @@ module Zxcvbn
|
|
|
6
8
|
# the lowercased password in the dictionary
|
|
7
9
|
|
|
8
10
|
class Dictionary
|
|
9
|
-
def initialize(name, ranked_dictionary)
|
|
11
|
+
def initialize(name, ranked_dictionary, trie = nil)
|
|
10
12
|
@name = name
|
|
11
13
|
@ranked_dictionary = ranked_dictionary
|
|
14
|
+
@trie = trie
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def matches(password)
|
|
18
|
+
lowercased_password = password.downcase
|
|
19
|
+
|
|
20
|
+
if @trie
|
|
21
|
+
trie_matches(password, lowercased_password)
|
|
22
|
+
else
|
|
23
|
+
hash_matches(password, lowercased_password)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def trie_matches(password, lowercased_password)
|
|
30
|
+
results = []
|
|
31
|
+
|
|
32
|
+
(0...password.length).each do |i|
|
|
33
|
+
@trie.search_prefixes(lowercased_password, i).each do |word, rank, start, ending|
|
|
34
|
+
results << build_match(word, password.slice(start, ending - start + 1), start, ending, rank)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
results
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hash_matches(password, lowercased_password)
|
|
15
42
|
results = []
|
|
16
43
|
password_length = password.length
|
|
17
|
-
|
|
44
|
+
|
|
18
45
|
(0..password_length).each do |i|
|
|
19
46
|
(i...password_length).each do |j|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
:j => j,
|
|
26
|
-
:rank => @ranked_dictionary[word],
|
|
27
|
-
:pattern => 'dictionary',
|
|
28
|
-
:dictionary_name => @name)
|
|
29
|
-
end
|
|
47
|
+
length = j - i + 1
|
|
48
|
+
word = lowercased_password.slice(i, length)
|
|
49
|
+
next unless @ranked_dictionary.key?(word)
|
|
50
|
+
|
|
51
|
+
results << build_match(word, password.slice(i, length), i, j, @ranked_dictionary[word])
|
|
30
52
|
end
|
|
31
53
|
end
|
|
54
|
+
|
|
32
55
|
results
|
|
33
56
|
end
|
|
57
|
+
|
|
58
|
+
def build_match(matched_word, token, start_pos, end_pos, rank)
|
|
59
|
+
Match.new(
|
|
60
|
+
matched_word: matched_word,
|
|
61
|
+
token: token,
|
|
62
|
+
i: start_pos,
|
|
63
|
+
j: end_pos,
|
|
64
|
+
rank: rank,
|
|
65
|
+
pattern: 'dictionary',
|
|
66
|
+
dictionary_name: @name
|
|
67
|
+
)
|
|
68
|
+
end
|
|
34
69
|
end
|
|
35
70
|
end
|
|
36
|
-
end
|
|
71
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/matchers/regex_helpers'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -5,7 +7,7 @@ module Zxcvbn
|
|
|
5
7
|
class Digits
|
|
6
8
|
include RegexHelpers
|
|
7
9
|
|
|
8
|
-
DIGITS_REGEX = /\d{3,}
|
|
10
|
+
DIGITS_REGEX = /\d{3,}/.freeze
|
|
9
11
|
|
|
10
12
|
def matches(password)
|
|
11
13
|
result = []
|
data/lib/zxcvbn/matchers/l33t.rb
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
1
5
|
module Zxcvbn
|
|
2
6
|
module Matchers
|
|
3
7
|
class L33t
|
|
4
8
|
L33T_TABLE = {
|
|
5
|
-
'a' => ['4', '@'],
|
|
6
|
-
'b' => ['8'],
|
|
7
|
-
'c' => ['(', '{', '[', '<'],
|
|
8
|
-
'e' => ['3'],
|
|
9
|
-
'g' => ['6', '9'],
|
|
10
|
-
'i' => ['1', '!', '|'],
|
|
11
|
-
'l' => ['1', '|', '7'],
|
|
12
|
-
'o' => ['0'],
|
|
13
|
-
's' => ['$', '5'],
|
|
14
|
-
't' => ['+', '7'],
|
|
15
|
-
'x' => ['%'],
|
|
16
|
-
'z' => ['2']
|
|
17
|
-
}
|
|
9
|
+
'a' => ['4', '@'].freeze,
|
|
10
|
+
'b' => ['8'].freeze,
|
|
11
|
+
'c' => ['(', '{', '[', '<'].freeze,
|
|
12
|
+
'e' => ['3'].freeze,
|
|
13
|
+
'g' => ['6', '9'].freeze,
|
|
14
|
+
'i' => ['1', '!', '|'].freeze,
|
|
15
|
+
'l' => ['1', '|', '7'].freeze,
|
|
16
|
+
'o' => ['0'].freeze,
|
|
17
|
+
's' => ['$', '5'].freeze,
|
|
18
|
+
't' => ['+', '7'].freeze,
|
|
19
|
+
'x' => ['%'].freeze,
|
|
20
|
+
'z' => ['2'].freeze
|
|
21
|
+
}.freeze
|
|
18
22
|
|
|
19
23
|
def initialize(dictionary_matchers)
|
|
20
24
|
@dictionary_matchers = dictionary_matchers
|
|
@@ -23,24 +27,17 @@ module Zxcvbn
|
|
|
23
27
|
def matches(password)
|
|
24
28
|
matches = []
|
|
25
29
|
lowercased_password = password.downcase
|
|
26
|
-
|
|
30
|
+
relevent_subtable = relevent_l33t_subtable(lowercased_password)
|
|
31
|
+
|
|
32
|
+
# Early bailout: if no l33t characters present, return empty matches
|
|
33
|
+
return matches if relevent_subtable.empty?
|
|
34
|
+
|
|
35
|
+
combinations_to_try = l33t_subs(relevent_subtable)
|
|
27
36
|
combinations_to_try.each do |substitution|
|
|
28
37
|
@dictionary_matchers.each do |matcher|
|
|
29
38
|
subbed_password = translate(lowercased_password, substitution)
|
|
30
39
|
matcher.matches(subbed_password).each do |match|
|
|
31
|
-
|
|
32
|
-
next if token.downcase == match.matched_word.downcase
|
|
33
|
-
match_substitutions = {}
|
|
34
|
-
substitution.each do |s, letter|
|
|
35
|
-
match_substitutions[s] = letter if token.include?(s)
|
|
36
|
-
end
|
|
37
|
-
match.l33t = true
|
|
38
|
-
match.token = password[match.i..match.j]
|
|
39
|
-
match.sub = match_substitutions
|
|
40
|
-
match.sub_display = match_substitutions.map do |k, v|
|
|
41
|
-
"#{k} -> #{v}"
|
|
42
|
-
end.join(', ')
|
|
43
|
-
matches << match
|
|
40
|
+
process_match(match, password, substitution, matches)
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
end
|
|
@@ -48,9 +45,11 @@ module Zxcvbn
|
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
def translate(password, sub)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
result = String.new
|
|
49
|
+
password.each_char do |chr|
|
|
50
|
+
result << (sub[chr] || chr)
|
|
51
|
+
end
|
|
52
|
+
result
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def relevent_l33t_subtable(password)
|
|
@@ -77,8 +76,29 @@ module Zxcvbn
|
|
|
77
76
|
new_subs
|
|
78
77
|
end
|
|
79
78
|
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def process_match(match, password, substitution, matches)
|
|
82
|
+
length = match.j - match.i + 1
|
|
83
|
+
token = password.slice(match.i, length)
|
|
84
|
+
return if token.downcase == match.matched_word.downcase
|
|
85
|
+
|
|
86
|
+
match_substitutions = {}
|
|
87
|
+
substitution.each do |s, letter|
|
|
88
|
+
match_substitutions[s] = letter if token.include?(s)
|
|
89
|
+
end
|
|
90
|
+
match.l33t = true
|
|
91
|
+
match.token = token
|
|
92
|
+
match.sub = match_substitutions
|
|
93
|
+
match.sub_display = match_substitutions.map do |k, v|
|
|
94
|
+
"#{k} -> #{v}"
|
|
95
|
+
end.join(', ')
|
|
96
|
+
matches << match
|
|
97
|
+
end
|
|
98
|
+
|
|
80
99
|
def find_substitutions(subs, table, keys)
|
|
81
100
|
return subs if keys.empty?
|
|
101
|
+
|
|
82
102
|
first_key = keys[0]
|
|
83
103
|
rest_keys = keys[1..-1]
|
|
84
104
|
next_subs = []
|
|
@@ -109,14 +129,12 @@ module Zxcvbn
|
|
|
109
129
|
|
|
110
130
|
def dedup(subs)
|
|
111
131
|
deduped = []
|
|
112
|
-
|
|
132
|
+
seen = Set.new
|
|
113
133
|
subs.each do |sub|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
unless members.include?(label)
|
|
119
|
-
members << label
|
|
134
|
+
# Sort and convert to hash for consistent comparison
|
|
135
|
+
sorted_sub = sub.sort.to_h
|
|
136
|
+
unless seen.include?(sorted_sub)
|
|
137
|
+
seen.add(sorted_sub)
|
|
120
138
|
deduped << sub
|
|
121
139
|
end
|
|
122
140
|
end
|
|
@@ -124,4 +142,4 @@ module Zxcvbn
|
|
|
124
142
|
end
|
|
125
143
|
end
|
|
126
144
|
end
|
|
127
|
-
end
|
|
145
|
+
end
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Zxcvbn
|
|
2
4
|
module Matchers
|
|
3
5
|
class L33t
|
|
4
6
|
L33T_TABLE = {
|
|
5
|
-
'a' => ['4', '@'],
|
|
6
|
-
'b' => ['8'],
|
|
7
|
-
'c' => ['(', '{', '[', '<'],
|
|
8
|
-
'e' => ['3'],
|
|
9
|
-
'g' => ['6', '9'],
|
|
10
|
-
'i' => ['1', '!', '|'],
|
|
11
|
-
'l' => ['1', '|', '7'],
|
|
12
|
-
'o' => ['0'],
|
|
13
|
-
's' => ['$', '5'],
|
|
14
|
-
't' => ['+', '7'],
|
|
15
|
-
'x' => ['%'],
|
|
16
|
-
'z' => ['2']
|
|
17
|
-
}
|
|
7
|
+
'a' => ['4', '@'].freeze,
|
|
8
|
+
'b' => ['8'].freeze,
|
|
9
|
+
'c' => ['(', '{', '[', '<'].freeze,
|
|
10
|
+
'e' => ['3'].freeze,
|
|
11
|
+
'g' => ['6', '9'].freeze,
|
|
12
|
+
'i' => ['1', '!', '|'].freeze,
|
|
13
|
+
'l' => ['1', '|', '7'].freeze,
|
|
14
|
+
'o' => ['0'].freeze,
|
|
15
|
+
's' => ['$', '5'].freeze,
|
|
16
|
+
't' => ['+', '7'].freeze,
|
|
17
|
+
'x' => ['%'].freeze,
|
|
18
|
+
'z' => ['2'].freeze
|
|
19
|
+
}.freeze
|
|
18
20
|
|
|
19
21
|
def initialize(dictionary_matchers)
|
|
20
22
|
@dictionary_matchers = dictionary_matchers
|
|
@@ -24,20 +26,20 @@ module Zxcvbn
|
|
|
24
26
|
matches = []
|
|
25
27
|
lowercased_password = password.downcase
|
|
26
28
|
combinations_to_try = substitution_combinations(relevant_l33t_substitutions(lowercased_password))
|
|
27
|
-
|
|
28
|
-
combinations_to_try.each do |substitution|
|
|
29
|
+
combinations_to_try.each do |substitutions|
|
|
29
30
|
@dictionary_matchers.each do |matcher|
|
|
30
|
-
subbed_password = substitute(lowercased_password,
|
|
31
|
+
subbed_password = substitute(lowercased_password, substitutions)
|
|
31
32
|
matcher.matches(subbed_password).each do |match|
|
|
32
|
-
|
|
33
|
+
length = match.j - match.i + 1
|
|
34
|
+
token = lowercased_password.slice(match.i, length)
|
|
33
35
|
next if token == match.matched_word.downcase
|
|
34
|
-
|
|
36
|
+
|
|
35
37
|
match_substitutions = {}
|
|
36
|
-
|
|
38
|
+
substitutions.each do |letter, substitution|
|
|
37
39
|
match_substitutions[substitution] = letter if token.include?(substitution)
|
|
38
40
|
end
|
|
39
41
|
match.l33t = true
|
|
40
|
-
match.token = password
|
|
42
|
+
match.token = password.slice(match.i, length)
|
|
41
43
|
match.sub = match_substitutions
|
|
42
44
|
match.sub_display = match_substitutions.map do |k, v|
|
|
43
45
|
"#{k} -> #{v}"
|
|
@@ -49,9 +51,9 @@ module Zxcvbn
|
|
|
49
51
|
matches
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
def substitute(password,
|
|
54
|
+
def substitute(password, substitutions)
|
|
53
55
|
subbed_password = password.dup
|
|
54
|
-
|
|
56
|
+
substitutions.each do |letter, substitution|
|
|
55
57
|
subbed_password.gsub!(substitution, letter)
|
|
56
58
|
end
|
|
57
59
|
subbed_password
|
|
@@ -64,9 +66,7 @@ module Zxcvbn
|
|
|
64
66
|
end
|
|
65
67
|
L33T_TABLE.each do |letter, substibutions|
|
|
66
68
|
password.each_char do |password_char|
|
|
67
|
-
if substibutions.include?(password_char)
|
|
68
|
-
subs[letter] << password_char
|
|
69
|
-
end
|
|
69
|
+
subs[letter] << password_char if substibutions.include?(password_char)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
subs
|
|
@@ -82,7 +82,7 @@ module Zxcvbn
|
|
|
82
82
|
expanded_substitutions.each do |substitution_hash|
|
|
83
83
|
# convert a hash to an array of hashes with 1 key each
|
|
84
84
|
subs_array = substitution_hash.map do |letter, substitutions|
|
|
85
|
-
{letter => substitutions}
|
|
85
|
+
{ letter => substitutions }
|
|
86
86
|
end
|
|
87
87
|
combinations << subs_array
|
|
88
88
|
|
|
@@ -94,15 +94,13 @@ module Zxcvbn
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# convert back to simple hash per substitution combination
|
|
97
|
-
|
|
97
|
+
combinations.map do |combination_set|
|
|
98
98
|
hash = {}
|
|
99
99
|
combination_set.each do |combination_hash|
|
|
100
100
|
hash.merge!(combination_hash)
|
|
101
101
|
end
|
|
102
102
|
hash
|
|
103
103
|
end
|
|
104
|
-
|
|
105
|
-
combination_hashes
|
|
106
104
|
end
|
|
107
105
|
|
|
108
106
|
# expand possible combinations if multiple characters can be substituted
|
|
@@ -110,11 +108,11 @@ module Zxcvbn
|
|
|
110
108
|
# [{'a' => '4', 'i' => 1}, {'a' => '@', 'i' => '1'}]
|
|
111
109
|
def expanded_substitutions(hash)
|
|
112
110
|
return {} if hash.empty?
|
|
111
|
+
|
|
113
112
|
values = hash.values
|
|
114
113
|
product_values = values[0].product(*values[1..-1])
|
|
115
|
-
product_values.map{ |p| Hash[hash.keys.zip(p)] }
|
|
114
|
+
product_values.map { |p| Hash[hash.keys.zip(p)] }
|
|
116
115
|
end
|
|
117
|
-
|
|
118
116
|
end
|
|
119
117
|
end
|
|
120
|
-
end
|
|
118
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -5,15 +7,15 @@ module Zxcvbn
|
|
|
5
7
|
module RegexHelpers
|
|
6
8
|
def re_match_all(regex, password)
|
|
7
9
|
pos = 0
|
|
8
|
-
while re_match = regex.match(password, pos)
|
|
10
|
+
while (re_match = regex.match(password, pos))
|
|
9
11
|
i, j = re_match.offset(0)
|
|
10
12
|
pos = j
|
|
11
13
|
j -= 1
|
|
12
14
|
|
|
13
15
|
match = Match.new(
|
|
14
|
-
:
|
|
15
|
-
:
|
|
16
|
-
:
|
|
16
|
+
i: i,
|
|
17
|
+
j: j,
|
|
18
|
+
token: password.slice(i, j - i + 1)
|
|
17
19
|
)
|
|
18
20
|
yield match, re_match
|
|
19
21
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -9,17 +11,15 @@ module Zxcvbn
|
|
|
9
11
|
while i < password.length
|
|
10
12
|
cur_char = password[i]
|
|
11
13
|
j = i + 1
|
|
12
|
-
while cur_char == password[j]
|
|
13
|
-
j += 1
|
|
14
|
-
end
|
|
14
|
+
j += 1 while cur_char == password[j]
|
|
15
15
|
|
|
16
16
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
17
17
|
result << Match.new(
|
|
18
|
-
:
|
|
19
|
-
:
|
|
20
|
-
:j
|
|
21
|
-
:
|
|
22
|
-
:
|
|
18
|
+
pattern: 'repeat',
|
|
19
|
+
i: i,
|
|
20
|
+
j: j - 1,
|
|
21
|
+
token: password.slice(i, j - i),
|
|
22
|
+
repeated_char: cur_char
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -7,14 +9,14 @@ module Zxcvbn
|
|
|
7
9
|
'lower' => 'abcdefghijklmnopqrstuvwxyz',
|
|
8
10
|
'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
9
11
|
'digits' => '01234567890'
|
|
10
|
-
}
|
|
12
|
+
}.freeze
|
|
11
13
|
|
|
12
14
|
def seq_match_length(password, from, direction, seq)
|
|
13
15
|
index_from = seq.index(password[from])
|
|
14
16
|
j = 1
|
|
15
17
|
while from + j < password.length &&
|
|
16
18
|
password[from + j] == seq[index_from + direction * j]
|
|
17
|
-
j+= 1
|
|
19
|
+
j += 1
|
|
18
20
|
end
|
|
19
21
|
j
|
|
20
22
|
end
|
|
@@ -24,15 +26,13 @@ module Zxcvbn
|
|
|
24
26
|
def applicable_sequence(password, i)
|
|
25
27
|
SEQUENCES.each do |name, sequence|
|
|
26
28
|
index1 = sequence.index(password[i])
|
|
27
|
-
index2 = sequence.index(password[i+1])
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
end
|
|
29
|
+
index2 = sequence.index(password[i + 1])
|
|
30
|
+
next unless index1 && index2
|
|
31
|
+
|
|
32
|
+
seq_direction = index2 - index1
|
|
33
|
+
return [name, sequence, seq_direction] if [-1, 1].include?(seq_direction)
|
|
34
|
+
|
|
35
|
+
return nil
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -46,13 +46,13 @@ module Zxcvbn
|
|
|
46
46
|
length = seq_match_length(password, i, seq_direction, seq)
|
|
47
47
|
if length > 2
|
|
48
48
|
result << Match.new(
|
|
49
|
-
:
|
|
50
|
-
:
|
|
51
|
-
:
|
|
52
|
-
:
|
|
53
|
-
:
|
|
54
|
-
:
|
|
55
|
-
:
|
|
49
|
+
pattern: 'sequence',
|
|
50
|
+
i: i,
|
|
51
|
+
j: i + length - 1,
|
|
52
|
+
token: password[i, length],
|
|
53
|
+
sequence_name: seq_name,
|
|
54
|
+
sequence_space: seq.length,
|
|
55
|
+
ascending: seq_direction == 1
|
|
56
56
|
)
|
|
57
57
|
end
|
|
58
58
|
i += length - 1
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -24,7 +26,7 @@ module Zxcvbn
|
|
|
24
26
|
turns = 0
|
|
25
27
|
shifted_count = 0
|
|
26
28
|
loop do
|
|
27
|
-
prev_char = password[j-1]
|
|
29
|
+
prev_char = password[j - 1]
|
|
28
30
|
found = false
|
|
29
31
|
found_direction = -1
|
|
30
32
|
cur_direction = -1
|
|
@@ -34,22 +36,22 @@ module Zxcvbn
|
|
|
34
36
|
cur_char = password[j]
|
|
35
37
|
adjacents.each do |adj|
|
|
36
38
|
cur_direction += 1
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
break
|
|
39
|
+
next unless adj&.index(cur_char)
|
|
40
|
+
|
|
41
|
+
found = true
|
|
42
|
+
found_direction = cur_direction
|
|
43
|
+
if adj.index(cur_char) == 1
|
|
44
|
+
# index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
|
|
45
|
+
# for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
|
|
46
|
+
shifted_count += 1
|
|
47
|
+
end
|
|
48
|
+
if last_direction != found_direction
|
|
49
|
+
# adding a turn is correct even in the initial case when last_direction is null:
|
|
50
|
+
# every spatial pattern starts with a turn.
|
|
51
|
+
turns += 1
|
|
52
|
+
last_direction = found_direction
|
|
52
53
|
end
|
|
54
|
+
break
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
# if the current pattern continued, extend j and try to grow again
|
|
@@ -59,13 +61,13 @@ module Zxcvbn
|
|
|
59
61
|
# otherwise push the pattern discovered so far, if any...
|
|
60
62
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
61
63
|
result << Match.new(
|
|
62
|
-
:
|
|
63
|
-
:
|
|
64
|
-
:j
|
|
65
|
-
:
|
|
66
|
-
:
|
|
67
|
-
:
|
|
68
|
-
:
|
|
64
|
+
pattern: 'spatial',
|
|
65
|
+
i: i,
|
|
66
|
+
j: j - 1,
|
|
67
|
+
token: password.slice(i, j - i),
|
|
68
|
+
graph: graph_name,
|
|
69
|
+
turns: turns,
|
|
70
|
+
shifted_count: shifted_count
|
|
69
71
|
)
|
|
70
72
|
end
|
|
71
73
|
# ...and then start a new search for the rest of the password.
|
|
@@ -78,4 +80,4 @@ module Zxcvbn
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
|
-
end
|
|
83
|
+
end
|
data/lib/zxcvbn/matchers/year.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/matchers/regex_helpers'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -5,7 +7,7 @@ module Zxcvbn
|
|
|
5
7
|
class Year
|
|
6
8
|
include RegexHelpers
|
|
7
9
|
|
|
8
|
-
YEAR_REGEX = /19\d\d|200\d|201\d
|
|
10
|
+
YEAR_REGEX = /19\d\d|200\d|201\d/.freeze
|
|
9
11
|
|
|
10
12
|
def matches(password)
|
|
11
13
|
result = []
|
|
@@ -17,4 +19,4 @@ module Zxcvbn
|
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
|
-
end
|
|
22
|
+
end
|
data/lib/zxcvbn/math.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Zxcvbn
|
|
2
4
|
module Math
|
|
3
5
|
def bruteforce_cardinality(password)
|
|
@@ -30,25 +32,27 @@ module Zxcvbn
|
|
|
30
32
|
|
|
31
33
|
def nCk(n, k)
|
|
32
34
|
return 0 if k > n
|
|
33
|
-
return 1 if k
|
|
35
|
+
return 1 if k.zero?
|
|
36
|
+
|
|
37
|
+
# Use symmetry property: C(n,k) = C(n, n-k)
|
|
38
|
+
# Choose smaller k to minimize iterations
|
|
39
|
+
k = n - k if k > n - k
|
|
40
|
+
|
|
34
41
|
r = 1
|
|
35
42
|
(1..k).each do |d|
|
|
36
|
-
r
|
|
37
|
-
r
|
|
43
|
+
r *= n
|
|
44
|
+
r /= d
|
|
38
45
|
n -= 1
|
|
39
46
|
end
|
|
40
47
|
r
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
def average_degree_for_graph(graph_name)
|
|
44
|
-
|
|
45
|
-
degrees = graph.map { |_, neighbors| neighbors.compact.size }
|
|
46
|
-
sum = degrees.inject(0, :+)
|
|
47
|
-
sum.to_f / graph.size
|
|
51
|
+
data.graph_stats[graph_name][:average_degree]
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def starting_positions_for_graph(graph_name)
|
|
51
|
-
data.
|
|
55
|
+
data.graph_stats[graph_name][:starting_positions]
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
end
|
data/lib/zxcvbn/omnimatch.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/dictionary_ranker'
|
|
2
4
|
require 'zxcvbn/matchers/dictionary'
|
|
3
5
|
require 'zxcvbn/matchers/l33t'
|
|
@@ -26,17 +28,18 @@ module Zxcvbn
|
|
|
26
28
|
|
|
27
29
|
def user_input_matchers(user_inputs)
|
|
28
30
|
return [] unless user_inputs.any?
|
|
31
|
+
|
|
29
32
|
user_ranked_dictionary = DictionaryRanker.rank_dictionary(user_inputs)
|
|
30
33
|
dictionary_matcher = Matchers::Dictionary.new('user_inputs', user_ranked_dictionary)
|
|
31
34
|
l33t_matcher = Matchers::L33t.new([dictionary_matcher])
|
|
32
35
|
[dictionary_matcher, l33t_matcher]
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
|
|
36
38
|
def build_matchers
|
|
37
39
|
matchers = []
|
|
38
40
|
dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary|
|
|
39
|
-
|
|
41
|
+
trie = @data.dictionary_tries[name]
|
|
42
|
+
Matchers::Dictionary.new(name, dictionary, trie)
|
|
40
43
|
end
|
|
41
44
|
l33t_matcher = Matchers::L33t.new(dictionary_matchers)
|
|
42
45
|
matchers += dictionary_matchers
|