zxcvbn-ruby 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|