zxcvbn-ruby 0.0.2 → 0.0.3
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.
- data/data/frequency_lists/english.txt +32545 -0
- data/data/frequency_lists/female_names.txt +3815 -0
- data/data/frequency_lists/male_names.txt +1004 -0
- data/data/frequency_lists/passwords.txt +7141 -0
- data/data/frequency_lists/surnames.txt +40583 -0
- data/lib/zxcvbn.rb +11 -2
- data/lib/zxcvbn/dictionary_ranker.rb +4 -9
- data/lib/zxcvbn/entropy.rb +25 -24
- data/lib/zxcvbn/match.rb +3 -5
- data/lib/zxcvbn/matchers/date.rb +11 -12
- data/lib/zxcvbn/matchers/digits.rb +1 -1
- data/lib/zxcvbn/matchers/regex_helpers.rb +5 -5
- data/lib/zxcvbn/matchers/repeat.rb +15 -17
- data/lib/zxcvbn/matchers/sequences.rb +45 -44
- data/lib/zxcvbn/math.rb +5 -14
- data/lib/zxcvbn/omnimatch.rb +5 -10
- data/lib/zxcvbn/scorer.rb +41 -34
- data/lib/zxcvbn/version.rb +1 -1
- data/spec/matchers/digits_spec.rb +1 -1
- data/spec/matchers/repeat_spec.rb +1 -1
- data/spec/matchers/sequences_spec.rb +7 -2
- data/spec/matchers/year_spec.rb +1 -1
- metadata +10 -6
- data/data/frequency_lists.yaml +0 -85094
data/lib/zxcvbn.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'pathname'
|
3
|
+
|
1
4
|
require 'zxcvbn/version'
|
2
5
|
require 'zxcvbn/match'
|
3
6
|
require 'zxcvbn/matchers/regex_helpers'
|
@@ -23,8 +26,14 @@ module Zxcvbn
|
|
23
26
|
|
24
27
|
DATA_PATH = Pathname(File.expand_path('../../data', __FILE__))
|
25
28
|
ADJACENCY_GRAPHS = JSON.load(DATA_PATH.join('adjacency_graphs.json').read)
|
26
|
-
|
27
|
-
RANKED_DICTIONARIES = DictionaryRanker.rank_dictionaries(
|
29
|
+
FREQUENCY_LISTS_PATH = DATA_PATH.join("frequency_lists")
|
30
|
+
RANKED_DICTIONARIES = DictionaryRanker.rank_dictionaries(
|
31
|
+
"english" => FREQUENCY_LISTS_PATH.join("english.txt").read.split,
|
32
|
+
"female_names" => FREQUENCY_LISTS_PATH.join("female_names.txt").read.split,
|
33
|
+
"male_names" => FREQUENCY_LISTS_PATH.join("male_names.txt").read.split,
|
34
|
+
"passwords" => FREQUENCY_LISTS_PATH.join("passwords.txt").read.split,
|
35
|
+
"surnames" => FREQUENCY_LISTS_PATH.join("surnames.txt").read.split
|
36
|
+
)
|
28
37
|
|
29
38
|
def test(password, user_inputs = [])
|
30
39
|
zxcvbn = PasswordStrength.new
|
@@ -3,21 +3,16 @@
|
|
3
3
|
module Zxcvbn
|
4
4
|
class DictionaryRanker
|
5
5
|
def self.rank_dictionaries(lists)
|
6
|
-
|
7
|
-
lists.each do |dict_name, words|
|
6
|
+
lists.each_with_object({}) do |(dict_name, words), dictionaries|
|
8
7
|
dictionaries[dict_name] = rank_dictionary(words)
|
9
8
|
end
|
10
|
-
dictionaries
|
11
9
|
end
|
12
10
|
|
13
11
|
def self.rank_dictionary(words)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
dictionary[word.downcase] = i
|
18
|
-
i += 1
|
12
|
+
words.each_with_index
|
13
|
+
.with_object({}) do |(word, i), dictionary|
|
14
|
+
dictionary[word.downcase] = i + 1
|
19
15
|
end
|
20
|
-
dictionary
|
21
16
|
end
|
22
17
|
end
|
23
18
|
end
|
data/lib/zxcvbn/entropy.rb
CHANGED
@@ -3,24 +3,25 @@ module Zxcvbn::Entropy
|
|
3
3
|
|
4
4
|
def calc_entropy(match)
|
5
5
|
return match.entropy unless match.entropy.nil?
|
6
|
-
|
6
|
+
|
7
7
|
match.entropy = case match.pattern
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
8
|
+
when 'repeat'
|
9
|
+
repeat_entropy(match)
|
10
|
+
when 'sequence'
|
11
|
+
sequence_entropy(match)
|
12
|
+
when 'digits'
|
13
|
+
digits_entropy(match)
|
14
|
+
when 'year'
|
15
|
+
year_entropy(match)
|
16
|
+
when 'date'
|
17
|
+
date_entropy(match)
|
18
|
+
when 'spatial'
|
19
|
+
spatial_entropy(match)
|
20
|
+
when 'dictionary'
|
21
|
+
dictionary_entropy(match)
|
22
|
+
else
|
23
|
+
0
|
24
|
+
end
|
24
25
|
end
|
25
26
|
|
26
27
|
def repeat_entropy(match)
|
@@ -66,7 +67,7 @@ module Zxcvbn::Entropy
|
|
66
67
|
entropy += 2
|
67
68
|
end
|
68
69
|
|
69
|
-
entropy
|
70
|
+
entropy
|
70
71
|
end
|
71
72
|
|
72
73
|
def dictionary_entropy(match)
|
@@ -90,7 +91,7 @@ module Zxcvbn::Entropy
|
|
90
91
|
num_upper = word.chars.count{|c| c.match(/[A-Z]/) }
|
91
92
|
num_lower = word.chars.count{|c| c.match(/[a-z]/) }
|
92
93
|
possibilities = 0
|
93
|
-
(0..
|
94
|
+
(0..[num_upper, num_lower].min).each do |i|
|
94
95
|
possibilities += nCk(num_upper + num_lower, i)
|
95
96
|
end
|
96
97
|
lg(possibilities)
|
@@ -103,7 +104,7 @@ module Zxcvbn::Entropy
|
|
103
104
|
match.sub.each do |subbed, unsubbed|
|
104
105
|
num_subbed = word.chars.count{|c| c == subbed}
|
105
106
|
num_unsubbed = word.chars.count{|c| c == unsubbed}
|
106
|
-
(0..
|
107
|
+
(0..[num_subbed, num_unsubbed].min).each do |i|
|
107
108
|
possibilities += nCk(num_subbed + num_unsubbed, i)
|
108
109
|
end
|
109
110
|
end
|
@@ -131,16 +132,16 @@ module Zxcvbn::Entropy
|
|
131
132
|
possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree ** j
|
132
133
|
end
|
133
134
|
end
|
134
|
-
|
135
|
+
|
135
136
|
entropy = lg possibilities
|
136
137
|
# add extra entropy for shifted keys. (% instead of 5, A instead of a.)
|
137
138
|
# math is similar to extra entropy from uppercase letters in dictionary matches.
|
138
|
-
|
139
|
+
|
139
140
|
if match.shifted_count
|
140
141
|
shiffted_count = match.shifted_count
|
141
142
|
unshifted_count = match.token.length - match.shifted_count
|
142
143
|
possibilities = 0
|
143
|
-
|
144
|
+
|
144
145
|
(0..[shiffted_count, unshifted_count].min).each do |i|
|
145
146
|
possibilities += nCk(shiffted_count + unshifted_count, i)
|
146
147
|
end
|
@@ -148,4 +149,4 @@ module Zxcvbn::Entropy
|
|
148
149
|
end
|
149
150
|
entropy
|
150
151
|
end
|
151
|
-
end
|
152
|
+
end
|
data/lib/zxcvbn/match.rb
CHANGED
@@ -3,11 +3,9 @@ require 'ostruct'
|
|
3
3
|
module Zxcvbn
|
4
4
|
class Match < OpenStruct
|
5
5
|
def to_hash
|
6
|
-
|
7
|
-
|
8
|
-
hash[key.to_s] = hash.delete(key)
|
6
|
+
@table.keys.sort.each_with_object({}) do |key, hash|
|
7
|
+
hash[key.to_s] = @table[key]
|
9
8
|
end
|
10
|
-
hash
|
11
9
|
end
|
12
10
|
end
|
13
|
-
end
|
11
|
+
end
|
data/lib/zxcvbn/matchers/date.rb
CHANGED
@@ -22,23 +22,23 @@ module Zxcvbn
|
|
22
22
|
WITHOUT_SEPARATOR = /\d{4,8}/
|
23
23
|
|
24
24
|
def matches(password)
|
25
|
-
|
26
|
-
result += match_with_separator(password)
|
27
|
-
result += match_without_separator(password)
|
28
|
-
result
|
25
|
+
match_with_separator(password) + match_without_separator(password)
|
29
26
|
end
|
30
27
|
|
31
28
|
def match_with_separator(password)
|
32
29
|
result = []
|
33
30
|
re_match_all(YEAR_SUFFIX, password) do |match, re_match|
|
34
31
|
match.pattern = 'date'
|
35
|
-
match.day = re_match[1].to_i
|
36
32
|
match.separator = re_match[2]
|
37
|
-
match.month = re_match[3].to_i
|
38
33
|
match.year = re_match[4].to_i
|
39
34
|
|
40
|
-
day
|
41
|
-
|
35
|
+
day = re_match[1].to_i
|
36
|
+
month = re_match[3].to_i
|
37
|
+
|
38
|
+
if month <= 12
|
39
|
+
match.day = day
|
40
|
+
match.month = month
|
41
|
+
else
|
42
42
|
match.day = month
|
43
43
|
match.month = day
|
44
44
|
end
|
@@ -54,7 +54,6 @@ module Zxcvbn
|
|
54
54
|
extract_dates(match.token).each do |candidate|
|
55
55
|
day, month, year = candidate[:day], candidate[:month], candidate[:year]
|
56
56
|
|
57
|
-
match = match.dup
|
58
57
|
match.pattern = 'date'
|
59
58
|
match.day = day
|
60
59
|
match.month = month
|
@@ -80,9 +79,9 @@ module Zxcvbn
|
|
80
79
|
candidate.each do |component, value|
|
81
80
|
candidate[component] = value.to_i
|
82
81
|
end
|
83
|
-
|
82
|
+
|
84
83
|
candidate[:year] = expand_year(candidate[:year])
|
85
|
-
|
84
|
+
|
86
85
|
if valid_date?(candidate[:day], candidate[:month], candidate[:year]) && !matches_year?(token)
|
87
86
|
dates << candidate
|
88
87
|
end
|
@@ -131,4 +130,4 @@ module Zxcvbn
|
|
131
130
|
end
|
132
131
|
end
|
133
132
|
end
|
134
|
-
end
|
133
|
+
end
|
@@ -2,20 +2,20 @@ module Zxcvbn
|
|
2
2
|
module Matchers
|
3
3
|
module RegexHelpers
|
4
4
|
def re_match_all(regex, password)
|
5
|
-
|
6
|
-
|
7
|
-
break unless re_match
|
5
|
+
pos = 0
|
6
|
+
while re_match = regex.match(password, pos)
|
8
7
|
i, j = re_match.offset(0)
|
8
|
+
pos = j
|
9
9
|
j -= 1
|
10
|
+
|
10
11
|
match = Match.new(
|
11
12
|
:i => i,
|
12
13
|
:j => j,
|
13
14
|
:token => password[i..j]
|
14
15
|
)
|
15
16
|
yield match, re_match
|
16
|
-
password = password.sub(re_match[0], ' ' * re_match[0].length)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
21
|
-
end
|
21
|
+
end
|
@@ -5,28 +5,26 @@ module Zxcvbn
|
|
5
5
|
result = []
|
6
6
|
i = 0
|
7
7
|
while i < password.length
|
8
|
+
cur_char = password[i]
|
8
9
|
j = i + 1
|
9
|
-
|
10
|
-
|
11
|
-
if password[j-1] == password[j]
|
12
|
-
j += 1
|
13
|
-
else
|
14
|
-
if j - i > 2 # don't consider length 1 or 2 chains.
|
15
|
-
result << Match.new(
|
16
|
-
:pattern => 'repeat',
|
17
|
-
:i => i,
|
18
|
-
:j => j-1,
|
19
|
-
:token => password[i...j],
|
20
|
-
:repeated_char => password[i]
|
21
|
-
)
|
22
|
-
end
|
23
|
-
break
|
24
|
-
end
|
10
|
+
while cur_char == password[j]
|
11
|
+
j += 1
|
25
12
|
end
|
13
|
+
|
14
|
+
if j - i > 2 # don't consider length 1 or 2 chains.
|
15
|
+
result << Match.new(
|
16
|
+
:pattern => 'repeat',
|
17
|
+
:i => i,
|
18
|
+
:j => j-1,
|
19
|
+
:token => password[i...j],
|
20
|
+
:repeated_char => cur_char
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
26
24
|
i = j
|
27
25
|
end
|
28
26
|
result
|
29
27
|
end
|
30
28
|
end
|
31
29
|
end
|
32
|
-
end
|
30
|
+
end
|
@@ -7,58 +7,59 @@ module Zxcvbn
|
|
7
7
|
'digits' => '01234567890'
|
8
8
|
}
|
9
9
|
|
10
|
+
def seq_match_length(password, from, direction, seq)
|
11
|
+
index_from = seq.index(password[from])
|
12
|
+
j = 1
|
13
|
+
while from + j < password.length &&
|
14
|
+
password[from + j] == seq[index_from + direction * j]
|
15
|
+
j+= 1
|
16
|
+
end
|
17
|
+
j
|
18
|
+
end
|
19
|
+
|
20
|
+
# find the first matching sequence, and return with
|
21
|
+
# direction, if characters are one apart in the sequence
|
22
|
+
def applicable_sequence(password, i)
|
23
|
+
SEQUENCES.each do |name, sequence|
|
24
|
+
index1 = sequence.index(password[i])
|
25
|
+
index2 = sequence.index(password[i+1])
|
26
|
+
if index1 and index2
|
27
|
+
seq_direction = index2 - index1
|
28
|
+
if [-1, 1].include?(seq_direction)
|
29
|
+
return [name, sequence, seq_direction]
|
30
|
+
else
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
10
37
|
def matches(password)
|
11
38
|
result = []
|
12
39
|
i = 0
|
13
|
-
while i < password.length-1
|
14
|
-
|
15
|
-
seq = nil # either lower, upper, or digits
|
16
|
-
seq_name = nil
|
17
|
-
seq_direction = nil # 1 for ascending seq abcd, -1 for dcba
|
18
|
-
SEQUENCES.each do |seq_candidate_name, seq_candidate|
|
19
|
-
seq = nil
|
40
|
+
while i < password.length - 1
|
41
|
+
seq_name, seq, seq_direction = applicable_sequence(password, i)
|
20
42
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
if seq
|
34
|
-
loop do
|
35
|
-
prev_char, cur_char = password[(j-1)], password[j]
|
36
|
-
prev_n, cur_n = [prev_char, cur_char].map do |chr|
|
37
|
-
chr ? seq_candidate.index(chr) : nil
|
38
|
-
end
|
39
|
-
if prev_n && cur_n && cur_n - prev_n == seq_direction
|
40
|
-
j += 1
|
41
|
-
else
|
42
|
-
if j - i > 2 # don't consider length 1 or 2 chains.
|
43
|
-
result << Match.new(
|
44
|
-
:pattern => 'sequence',
|
45
|
-
:i => i,
|
46
|
-
:j => j-1,
|
47
|
-
:token => password[i...j],
|
48
|
-
:sequence_name => seq_name,
|
49
|
-
:sequence_space => seq.length,
|
50
|
-
:ascending => seq_direction == 1
|
51
|
-
)
|
52
|
-
end
|
53
|
-
break
|
54
|
-
end
|
55
|
-
end
|
43
|
+
if seq
|
44
|
+
length = seq_match_length(password, i, seq_direction, seq)
|
45
|
+
if length > 2
|
46
|
+
result << Match.new(
|
47
|
+
:pattern => 'sequence',
|
48
|
+
:i => i,
|
49
|
+
:j => i + length - 1,
|
50
|
+
:token => password[i, length],
|
51
|
+
:sequence_name => seq_name,
|
52
|
+
:sequence_space => seq.length,
|
53
|
+
:ascending => seq_direction == 1
|
54
|
+
)
|
56
55
|
end
|
56
|
+
i += length - 1
|
57
|
+
else
|
58
|
+
i += 1
|
57
59
|
end
|
58
|
-
i = j
|
59
60
|
end
|
60
61
|
result
|
61
62
|
end
|
62
63
|
end
|
63
64
|
end
|
64
|
-
end
|
65
|
+
end
|
data/lib/zxcvbn/math.rb
CHANGED
@@ -40,24 +40,15 @@ module Zxcvbn
|
|
40
40
|
r
|
41
41
|
end
|
42
42
|
|
43
|
-
def min(a, b)
|
44
|
-
a < b ? a : b
|
45
|
-
end
|
46
|
-
|
47
43
|
def average_degree_for_graph(graph_name)
|
48
|
-
graph
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
average += neighbors.compact.length
|
53
|
-
end
|
54
|
-
|
55
|
-
average /= graph.keys.length
|
56
|
-
average
|
44
|
+
graph = Zxcvbn::ADJACENCY_GRAPHS[graph_name]
|
45
|
+
degrees = graph.map { |_, neighbors| neighbors.compact.size }
|
46
|
+
sum = degrees.inject(0, :+)
|
47
|
+
sum.to_f / graph.size
|
57
48
|
end
|
58
49
|
|
59
50
|
def starting_positions_for_graph(graph_name)
|
60
51
|
Zxcvbn::ADJACENCY_GRAPHS[graph_name].length
|
61
52
|
end
|
62
53
|
end
|
63
|
-
end
|
54
|
+
end
|
data/lib/zxcvbn/omnimatch.rb
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'yaml'
|
3
|
-
require 'pathname'
|
4
|
-
|
5
1
|
module Zxcvbn
|
6
2
|
class Omnimatch
|
7
3
|
def initialize
|
@@ -9,11 +5,10 @@ module Zxcvbn
|
|
9
5
|
end
|
10
6
|
|
11
7
|
def matches(password, user_inputs = [])
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
result
|
8
|
+
matchers = @matchers + user_input_matchers(user_inputs)
|
9
|
+
matchers.map do |matcher|
|
10
|
+
matcher.matches(password)
|
11
|
+
end.inject(&:+)
|
17
12
|
end
|
18
13
|
|
19
14
|
private
|
@@ -46,4 +41,4 @@ module Zxcvbn
|
|
46
41
|
matchers
|
47
42
|
end
|
48
43
|
end
|
49
|
-
end
|
44
|
+
end
|