zxcvbn-ruby 0.0.3 → 1.1.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 +7 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +42 -0
- data/CODE_OF_CONDUCT.md +130 -0
- data/Gemfile +8 -1
- data/Guardfile +26 -0
- data/{LICENSE → LICENSE.txt} +0 -0
- data/README.md +165 -9
- data/Rakefile +5 -1
- data/lib/zxcvbn.rb +10 -36
- data/lib/zxcvbn/crack_time.rb +44 -42
- data/lib/zxcvbn/data.rb +29 -0
- data/lib/zxcvbn/dictionary_ranker.rb +0 -2
- data/lib/zxcvbn/entropy.rb +3 -1
- data/lib/zxcvbn/feedback.rb +10 -0
- data/lib/zxcvbn/feedback_giver.rb +133 -0
- data/lib/zxcvbn/matchers/date.rb +2 -0
- data/lib/zxcvbn/matchers/dictionary.rb +2 -0
- data/lib/zxcvbn/matchers/digits.rb +2 -0
- data/lib/zxcvbn/matchers/l33t.rb +2 -2
- data/lib/zxcvbn/matchers/regex_helpers.rb +2 -0
- data/lib/zxcvbn/matchers/repeat.rb +2 -0
- data/lib/zxcvbn/matchers/sequences.rb +2 -0
- data/lib/zxcvbn/matchers/spatial.rb +2 -0
- data/lib/zxcvbn/matchers/year.rb +2 -0
- data/lib/zxcvbn/math.rb +2 -2
- data/lib/zxcvbn/omnimatch.rb +14 -3
- data/lib/zxcvbn/password_strength.rb +7 -3
- data/lib/zxcvbn/score.rb +1 -1
- data/lib/zxcvbn/scorer.rb +11 -0
- data/lib/zxcvbn/tester.rb +43 -0
- data/lib/zxcvbn/version.rb +1 -1
- data/spec/dictionary_ranker_spec.rb +2 -2
- data/spec/feedback_giver_spec.rb +212 -0
- data/spec/matchers/date_spec.rb +8 -8
- data/spec/matchers/dictionary_spec.rb +25 -14
- data/spec/matchers/digits_spec.rb +3 -3
- data/spec/matchers/l33t_spec.rb +15 -13
- data/spec/matchers/repeat_spec.rb +6 -6
- data/spec/matchers/sequences_spec.rb +5 -5
- data/spec/matchers/spatial_spec.rb +8 -8
- data/spec/matchers/year_spec.rb +3 -3
- data/spec/omnimatch_spec.rb +2 -2
- data/spec/scoring/crack_time_spec.rb +13 -13
- data/spec/scoring/entropy_spec.rb +28 -25
- data/spec/scoring/math_spec.rb +22 -18
- data/spec/support/matcher.rb +1 -1
- data/spec/tester_spec.rb +99 -0
- data/spec/zxcvbn_spec.rb +14 -39
- data/zxcvbn-ruby.gemspec +11 -0
- metadata +34 -29
data/lib/zxcvbn/crack_time.rb
CHANGED
@@ -1,51 +1,53 @@
|
|
1
|
-
module Zxcvbn
|
2
|
-
|
3
|
-
|
1
|
+
module Zxcvbn
|
2
|
+
module CrackTime
|
3
|
+
SINGLE_GUESS = 0.010
|
4
|
+
NUM_ATTACKERS = 100
|
4
5
|
|
5
|
-
|
6
|
+
SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
def entropy_to_crack_time(entropy)
|
9
|
+
0.5 * (2 ** entropy) * SECONDS_PER_GUESS
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
def crack_time_to_score(seconds)
|
13
|
+
case
|
14
|
+
when seconds < 10**2
|
15
|
+
0
|
16
|
+
when seconds < 10**4
|
17
|
+
1
|
18
|
+
when seconds < 10**6
|
19
|
+
2
|
20
|
+
when seconds < 10**8
|
21
|
+
3
|
22
|
+
else
|
23
|
+
4
|
24
|
+
end
|
23
25
|
end
|
24
|
-
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
def display_time(seconds)
|
28
|
+
minute = 60
|
29
|
+
hour = minute * 60
|
30
|
+
day = hour * 24
|
31
|
+
month = day * 31
|
32
|
+
year = month * 12
|
33
|
+
century = year * 100
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
35
|
+
case
|
36
|
+
when seconds < minute
|
37
|
+
'instant'
|
38
|
+
when seconds < hour
|
39
|
+
"#{1 + (seconds / minute).ceil} minutes"
|
40
|
+
when seconds < day
|
41
|
+
"#{1 + (seconds / hour).ceil} hours"
|
42
|
+
when seconds < month
|
43
|
+
"#{1 + (seconds / day).ceil} days"
|
44
|
+
when seconds < year
|
45
|
+
"#{1 + (seconds / month).ceil} months"
|
46
|
+
when seconds < century
|
47
|
+
"#{1 + (seconds / year).ceil} years"
|
48
|
+
else
|
49
|
+
'centuries'
|
50
|
+
end
|
49
51
|
end
|
50
52
|
end
|
51
53
|
end
|
data/lib/zxcvbn/data.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'zxcvbn/dictionary_ranker'
|
3
|
+
|
4
|
+
module Zxcvbn
|
5
|
+
class Data
|
6
|
+
def initialize
|
7
|
+
@ranked_dictionaries = DictionaryRanker.rank_dictionaries(
|
8
|
+
"english" => read_word_list("english.txt"),
|
9
|
+
"female_names" => read_word_list("female_names.txt"),
|
10
|
+
"male_names" => read_word_list("male_names.txt"),
|
11
|
+
"passwords" => read_word_list("passwords.txt"),
|
12
|
+
"surnames" => read_word_list("surnames.txt")
|
13
|
+
)
|
14
|
+
@adjacency_graphs = JSON.load(DATA_PATH.join('adjacency_graphs.json').read)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :ranked_dictionaries, :adjacency_graphs
|
18
|
+
|
19
|
+
def add_word_list(name, list)
|
20
|
+
@ranked_dictionaries[name] = DictionaryRanker.rank_dictionary(list)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def read_word_list(file)
|
26
|
+
DATA_PATH.join("frequency_lists", file).read.split
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/zxcvbn/entropy.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'zxcvbn/math'
|
2
|
+
|
1
3
|
module Zxcvbn::Entropy
|
2
4
|
include Zxcvbn::Math
|
3
5
|
|
@@ -127,7 +129,7 @@ module Zxcvbn::Entropy
|
|
127
129
|
|
128
130
|
# estimate the ngpumber of possible patterns w/ token length or less with number of turns or less.
|
129
131
|
(2..token_length).each do |i|
|
130
|
-
possible_turns = [turns, i -1].min
|
132
|
+
possible_turns = [turns, i - 1].min
|
131
133
|
(1..possible_turns).each do |j|
|
132
134
|
possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree ** j
|
133
135
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'zxcvbn/entropy'
|
2
|
+
require 'zxcvbn/feedback'
|
3
|
+
|
4
|
+
module Zxcvbn
|
5
|
+
class FeedbackGiver
|
6
|
+
NAME_DICTIONARIES = %w[surnames male_names female_names].freeze
|
7
|
+
|
8
|
+
DEFAULT_FEEDBACK = Feedback.new(
|
9
|
+
suggestions: [
|
10
|
+
'Use a few words, avoid common phrases',
|
11
|
+
'No need for symbols, digits, or uppercase letters'
|
12
|
+
]
|
13
|
+
).freeze
|
14
|
+
|
15
|
+
EMPTY_FEEDBACK = Feedback.new.freeze
|
16
|
+
|
17
|
+
def self.get_feedback(score, sequence)
|
18
|
+
# starting feedback
|
19
|
+
return DEFAULT_FEEDBACK if sequence.length.zero?
|
20
|
+
|
21
|
+
# no feedback if score is good or great.
|
22
|
+
return EMPTY_FEEDBACK if score > 2
|
23
|
+
|
24
|
+
# tie feedback to the longest match for longer sequences
|
25
|
+
longest_match = sequence[0]
|
26
|
+
for match in sequence[1..-1]
|
27
|
+
longest_match = match if match.token.length > longest_match.token.length
|
28
|
+
end
|
29
|
+
|
30
|
+
feedback = get_match_feedback(longest_match, sequence.length == 1)
|
31
|
+
extra_feedback = 'Add another word or two. Uncommon words are better.'
|
32
|
+
|
33
|
+
if feedback.nil?
|
34
|
+
feedback = Feedback.new(suggestions: [extra_feedback])
|
35
|
+
else
|
36
|
+
feedback.suggestions.unshift extra_feedback
|
37
|
+
end
|
38
|
+
|
39
|
+
feedback
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.get_match_feedback(match, is_sole_match)
|
43
|
+
case match.pattern
|
44
|
+
when 'dictionary'
|
45
|
+
get_dictionary_match_feedback match, is_sole_match
|
46
|
+
|
47
|
+
when 'spatial'
|
48
|
+
layout = match.graph.upcase
|
49
|
+
warning = if match.turns == 1
|
50
|
+
'Straight rows of keys are easy to guess'
|
51
|
+
else
|
52
|
+
'Short keyboard patterns are easy to guess'
|
53
|
+
end
|
54
|
+
|
55
|
+
Feedback.new(
|
56
|
+
warning: warning,
|
57
|
+
suggestions: [
|
58
|
+
'Use a longer keyboard pattern with more turns'
|
59
|
+
]
|
60
|
+
)
|
61
|
+
|
62
|
+
when 'repeat'
|
63
|
+
Feedback.new(
|
64
|
+
warning: 'Repeats like "aaa" are easy to guess',
|
65
|
+
suggestions: [
|
66
|
+
'Avoid repeated words and characters'
|
67
|
+
]
|
68
|
+
)
|
69
|
+
|
70
|
+
when 'sequence'
|
71
|
+
Feedback.new(
|
72
|
+
warning: 'Sequences like abc or 6543 are easy to guess',
|
73
|
+
suggestions: [
|
74
|
+
'Avoid sequences'
|
75
|
+
]
|
76
|
+
)
|
77
|
+
|
78
|
+
when 'date'
|
79
|
+
Feedback.new(
|
80
|
+
warning: 'Dates are often easy to guess',
|
81
|
+
suggestions: [
|
82
|
+
'Avoid dates and years that are associated with you'
|
83
|
+
]
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.get_dictionary_match_feedback(match, is_sole_match)
|
89
|
+
warning = if match.dictionary_name == 'passwords'
|
90
|
+
if is_sole_match && !match.l33t && !match.reversed
|
91
|
+
if match.rank <= 10
|
92
|
+
'This is a top-10 common password'
|
93
|
+
elsif match.rank <= 100
|
94
|
+
'This is a top-100 common password'
|
95
|
+
else
|
96
|
+
'This is a very common password'
|
97
|
+
end
|
98
|
+
else
|
99
|
+
'This is similar to a commonly used password'
|
100
|
+
end
|
101
|
+
elsif NAME_DICTIONARIES.include? match.dictionary_name
|
102
|
+
if is_sole_match
|
103
|
+
'Names and surnames by themselves are easy to guess'
|
104
|
+
else
|
105
|
+
'Common names and surnames are easy to guess'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
suggestions = []
|
110
|
+
word = match.token
|
111
|
+
|
112
|
+
if word =~ Zxcvbn::Entropy::START_UPPER
|
113
|
+
suggestions.push "Capitalization doesn't help very much"
|
114
|
+
elsif word =~ Zxcvbn::Entropy::ALL_UPPER && word.downcase != word
|
115
|
+
suggestions.push(
|
116
|
+
'All-uppercase is almost as easy to guess as all-lowercase'
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
if match.l33t
|
121
|
+
suggestions.push(
|
122
|
+
"Predictable substitutions like '@' instead of 'a' \
|
123
|
+
don't help very much"
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
Feedback.new(
|
128
|
+
warning: warning,
|
129
|
+
suggestions: suggestions
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/zxcvbn/matchers/date.rb
CHANGED
data/lib/zxcvbn/matchers/l33t.rb
CHANGED
@@ -31,8 +31,8 @@ module Zxcvbn
|
|
31
31
|
token = password[match.i..match.j]
|
32
32
|
next if token.downcase == match.matched_word.downcase
|
33
33
|
match_substitutions = {}
|
34
|
-
substitution.each do |
|
35
|
-
match_substitutions[
|
34
|
+
substitution.each do |s, letter|
|
35
|
+
match_substitutions[s] = letter if token.include?(s)
|
36
36
|
end
|
37
37
|
match.l33t = true
|
38
38
|
match.token = password[match.i..match.j]
|
data/lib/zxcvbn/matchers/year.rb
CHANGED
data/lib/zxcvbn/math.rb
CHANGED
@@ -41,14 +41,14 @@ module Zxcvbn
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def average_degree_for_graph(graph_name)
|
44
|
-
graph =
|
44
|
+
graph = data.adjacency_graphs[graph_name]
|
45
45
|
degrees = graph.map { |_, neighbors| neighbors.compact.size }
|
46
46
|
sum = degrees.inject(0, :+)
|
47
47
|
sum.to_f / graph.size
|
48
48
|
end
|
49
49
|
|
50
50
|
def starting_positions_for_graph(graph_name)
|
51
|
-
|
51
|
+
data.adjacency_graphs[graph_name].length
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
data/lib/zxcvbn/omnimatch.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
|
+
require 'zxcvbn/dictionary_ranker'
|
2
|
+
require 'zxcvbn/matchers/dictionary'
|
3
|
+
require 'zxcvbn/matchers/l33t'
|
4
|
+
require 'zxcvbn/matchers/spatial'
|
5
|
+
require 'zxcvbn/matchers/digits'
|
6
|
+
require 'zxcvbn/matchers/repeat'
|
7
|
+
require 'zxcvbn/matchers/sequences'
|
8
|
+
require 'zxcvbn/matchers/year'
|
9
|
+
require 'zxcvbn/matchers/date'
|
10
|
+
|
1
11
|
module Zxcvbn
|
2
12
|
class Omnimatch
|
3
|
-
def initialize
|
13
|
+
def initialize(data)
|
14
|
+
@data = data
|
4
15
|
@matchers = build_matchers
|
5
16
|
end
|
6
17
|
|
@@ -24,14 +35,14 @@ module Zxcvbn
|
|
24
35
|
|
25
36
|
def build_matchers
|
26
37
|
matchers = []
|
27
|
-
dictionary_matchers =
|
38
|
+
dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary|
|
28
39
|
Matchers::Dictionary.new(name, dictionary)
|
29
40
|
end
|
30
41
|
l33t_matcher = Matchers::L33t.new(dictionary_matchers)
|
31
42
|
matchers += dictionary_matchers
|
32
43
|
matchers += [
|
33
44
|
l33t_matcher,
|
34
|
-
Matchers::Spatial.new(
|
45
|
+
Matchers::Spatial.new(@data.adjacency_graphs),
|
35
46
|
Matchers::Digits.new,
|
36
47
|
Matchers::Repeat.new,
|
37
48
|
Matchers::Sequences.new,
|
@@ -1,10 +1,13 @@
|
|
1
1
|
require 'benchmark'
|
2
|
+
require 'zxcvbn/feedback_giver'
|
3
|
+
require 'zxcvbn/omnimatch'
|
4
|
+
require 'zxcvbn/scorer'
|
2
5
|
|
3
6
|
module Zxcvbn
|
4
7
|
class PasswordStrength
|
5
|
-
def initialize
|
6
|
-
@omnimatch = Omnimatch.new
|
7
|
-
@scorer = Scorer.new
|
8
|
+
def initialize(data)
|
9
|
+
@omnimatch = Omnimatch.new(data)
|
10
|
+
@scorer = Scorer.new(data)
|
8
11
|
end
|
9
12
|
|
10
13
|
def test(password, user_inputs = [])
|
@@ -15,6 +18,7 @@ module Zxcvbn
|
|
15
18
|
result = @scorer.minimum_entropy_match_sequence(password, matches)
|
16
19
|
end
|
17
20
|
result.calc_time = calc_time
|
21
|
+
result.feedback = FeedbackGiver.get_feedback(result.score, result.match_sequence)
|
18
22
|
result
|
19
23
|
end
|
20
24
|
end
|
data/lib/zxcvbn/score.rb
CHANGED