zxcvbn-ruby 0.1.0 → 1.2.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/.github/workflows/ci.yml +23 -0
- data/CHANGELOG.md +58 -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 +152 -12
- data/Rakefile +5 -1
- data/lib/zxcvbn/entropy.rb +1 -1
- data/lib/zxcvbn/feedback.rb +10 -0
- data/lib/zxcvbn/feedback_giver.rb +133 -0
- data/lib/zxcvbn/matchers/l33t.rb +2 -2
- data/lib/zxcvbn/password_strength.rb +2 -0
- data/lib/zxcvbn/score.rb +1 -1
- data/lib/zxcvbn/tester.rb +13 -5
- data/lib/zxcvbn/version.rb +3 -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 +14 -12
- data/spec/matchers/repeat_spec.rb +6 -6
- data/spec/matchers/sequences_spec.rb +5 -5
- data/spec/matchers/spatial_spec.rb +7 -7
- data/spec/matchers/year_spec.rb +3 -3
- data/spec/omnimatch_spec.rb +1 -1
- data/spec/scoring/crack_time_spec.rb +13 -13
- data/spec/scoring/entropy_spec.rb +24 -24
- data/spec/scoring/math_spec.rb +18 -18
- data/spec/support/js_helpers.rb +3 -4
- data/spec/support/matcher.rb +1 -1
- data/spec/tester_spec.rb +75 -27
- data/zxcvbn-ruby.gemspec +14 -1
- metadata +34 -33
data/lib/zxcvbn/entropy.rb
CHANGED
@@ -129,7 +129,7 @@ module Zxcvbn::Entropy
|
|
129
129
|
|
130
130
|
# estimate the ngpumber of possible patterns w/ token length or less with number of turns or less.
|
131
131
|
(2..token_length).each do |i|
|
132
|
-
possible_turns = [turns, i -1].min
|
132
|
+
possible_turns = [turns, i - 1].min
|
133
133
|
(1..possible_turns).each do |j|
|
134
134
|
possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree ** j
|
135
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/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]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'benchmark'
|
2
|
+
require 'zxcvbn/feedback_giver'
|
2
3
|
require 'zxcvbn/omnimatch'
|
3
4
|
require 'zxcvbn/scorer'
|
4
5
|
|
@@ -17,6 +18,7 @@ module Zxcvbn
|
|
17
18
|
result = @scorer.minimum_entropy_match_sequence(password, matches)
|
18
19
|
end
|
19
20
|
result.calc_time = calc_time
|
21
|
+
result.feedback = FeedbackGiver.get_feedback(result.score, result.match_sequence)
|
20
22
|
result
|
21
23
|
end
|
22
24
|
end
|
data/lib/zxcvbn/score.rb
CHANGED
data/lib/zxcvbn/tester.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zxcvbn/data"
|
4
|
+
require "zxcvbn/password_strength"
|
3
5
|
|
4
6
|
module Zxcvbn
|
5
7
|
# Allows you to test the strength of multiple passwords without reading and
|
@@ -21,15 +23,21 @@ module Zxcvbn
|
|
21
23
|
end
|
22
24
|
|
23
25
|
def test(password, user_inputs = [])
|
24
|
-
PasswordStrength.new(@data).test(password, user_inputs)
|
26
|
+
PasswordStrength.new(@data).test(password, sanitize(user_inputs))
|
25
27
|
end
|
26
28
|
|
27
29
|
def add_word_lists(lists)
|
28
|
-
lists.each_pair {|name, words| @data.add_word_list(name, words)}
|
30
|
+
lists.each_pair { |name, words| @data.add_word_list(name, sanitize(words)) }
|
29
31
|
end
|
30
32
|
|
31
33
|
def inspect
|
32
|
-
"#<#{self.class}:0x#{
|
34
|
+
"#<#{self.class}:0x#{__id__.to_s(16)}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def sanitize(user_inputs)
|
40
|
+
user_inputs.select { |i| i.respond_to?(:downcase) }
|
33
41
|
end
|
34
42
|
end
|
35
43
|
end
|
data/lib/zxcvbn/version.rb
CHANGED
@@ -5,8 +5,8 @@ describe Zxcvbn::DictionaryRanker do
|
|
5
5
|
it 'ranks word lists' do
|
6
6
|
result = Zxcvbn::DictionaryRanker.rank_dictionaries({:test => ['ABC', 'def'],
|
7
7
|
:test2 => ['def', 'ABC']})
|
8
|
-
result[:test].
|
9
|
-
result[:test2].
|
8
|
+
expect(result[:test]).to eq({'abc' => 1, 'def' => 2})
|
9
|
+
expect(result[:test2]).to eq({'def' => 1, 'abc' => 2})
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::FeedbackGiver do
|
4
|
+
# NOTE: We go in via the tester because the `FeedbackGiver` relies on both
|
5
|
+
# Omnimatch and the Scorer, which are troublesome to wire up for tests
|
6
|
+
let(:tester) { Zxcvbn::Tester.new }
|
7
|
+
|
8
|
+
describe '.get_feedback' do
|
9
|
+
it "gives empty feedback when a password's score is good" do
|
10
|
+
feedback = tester.test('5815A30BE798').feedback
|
11
|
+
|
12
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
13
|
+
expect(feedback.warning).to be_nil
|
14
|
+
expect(feedback.suggestions).to be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'gives general feedback when a password is empty' do
|
18
|
+
feedback = tester.test('').feedback
|
19
|
+
|
20
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
21
|
+
expect(feedback.warning).to be_nil
|
22
|
+
expect(feedback.suggestions).to contain_exactly(
|
23
|
+
'Use a few words, avoid common phrases',
|
24
|
+
'No need for symbols, digits, or uppercase letters'
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "gives general feedback when a password is poor but doesn't match any heuristics" do
|
29
|
+
feedback = tester.test(':005:0').feedback
|
30
|
+
|
31
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
32
|
+
expect(feedback.warning).to be_nil
|
33
|
+
expect(feedback.suggestions).to contain_exactly(
|
34
|
+
'Add another word or two. Uncommon words are better.'
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'gives specific feedback' do
|
39
|
+
describe 'for dictionary passwords' do
|
40
|
+
it 'that are extremely common' do
|
41
|
+
feedback = tester.test('password').feedback
|
42
|
+
|
43
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
44
|
+
expect(feedback.warning).to eql('This is a top-10 common password')
|
45
|
+
expect(feedback.suggestions).to contain_exactly(
|
46
|
+
'Add another word or two. Uncommon words are better.'
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'that are very, very common' do
|
51
|
+
feedback = tester.test('letmein').feedback
|
52
|
+
|
53
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
54
|
+
expect(feedback.warning).to eql('This is a top-100 common password')
|
55
|
+
expect(feedback.suggestions).to contain_exactly(
|
56
|
+
'Add another word or two. Uncommon words are better.'
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'that are very common' do
|
61
|
+
feedback = tester.test('playstation').feedback
|
62
|
+
|
63
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
64
|
+
expect(feedback.warning).to eql('This is a very common password')
|
65
|
+
expect(feedback.suggestions).to contain_exactly(
|
66
|
+
'Add another word or two. Uncommon words are better.'
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'that are common and you tried to use l33tsp33k' do
|
71
|
+
feedback = tester.test('pl4yst4ti0n').feedback
|
72
|
+
|
73
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
74
|
+
expect(feedback.warning).to eql(
|
75
|
+
'This is similar to a commonly used password'
|
76
|
+
)
|
77
|
+
expect(feedback.suggestions).to contain_exactly(
|
78
|
+
'Add another word or two. Uncommon words are better.',
|
79
|
+
"Predictable substitutions like '@' instead of 'a' don't help very much"
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'that are common and you capitalised the start' do
|
84
|
+
feedback = tester.test('Password').feedback
|
85
|
+
|
86
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
87
|
+
expect(feedback.warning).to eql(
|
88
|
+
'This is a top-10 common password'
|
89
|
+
)
|
90
|
+
expect(feedback.suggestions).to contain_exactly(
|
91
|
+
'Add another word or two. Uncommon words are better.',
|
92
|
+
"Capitalization doesn't help very much"
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'that are common and you capitalised the whole thing' do
|
97
|
+
feedback = tester.test('PASSWORD').feedback
|
98
|
+
|
99
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
100
|
+
expect(feedback.warning).to eql(
|
101
|
+
'This is a top-10 common password'
|
102
|
+
)
|
103
|
+
expect(feedback.suggestions).to contain_exactly(
|
104
|
+
'Add another word or two. Uncommon words are better.',
|
105
|
+
'All-uppercase is almost as easy to guess as all-lowercase'
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'that contain a common first name or last name' do
|
110
|
+
feedback = tester.test('jessica').feedback
|
111
|
+
|
112
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
113
|
+
expect(feedback.warning).to eql(
|
114
|
+
'Names and surnames by themselves are easy to guess'
|
115
|
+
)
|
116
|
+
expect(feedback.suggestions).to contain_exactly(
|
117
|
+
'Add another word or two. Uncommon words are better.'
|
118
|
+
)
|
119
|
+
|
120
|
+
feedback = tester.test('smith').feedback
|
121
|
+
|
122
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
123
|
+
expect(feedback.warning).to eql(
|
124
|
+
'Names and surnames by themselves are easy to guess'
|
125
|
+
)
|
126
|
+
expect(feedback.suggestions).to contain_exactly(
|
127
|
+
'Add another word or two. Uncommon words are better.'
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'that contain a common name and surname' do
|
132
|
+
feedback = tester.test('jessica smith').feedback
|
133
|
+
|
134
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
135
|
+
expect(feedback.warning).to eql(
|
136
|
+
'Common names and surnames are easy to guess'
|
137
|
+
)
|
138
|
+
expect(feedback.suggestions).to contain_exactly(
|
139
|
+
'Add another word or two. Uncommon words are better.'
|
140
|
+
)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe 'for spatial passwords' do
|
145
|
+
it 'that contain a straight keyboard row' do
|
146
|
+
feedback = tester.test('1qaz').feedback
|
147
|
+
|
148
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
149
|
+
expect(feedback.warning).to eql(
|
150
|
+
'Straight rows of keys are easy to guess'
|
151
|
+
)
|
152
|
+
expect(feedback.suggestions).to contain_exactly(
|
153
|
+
'Add another word or two. Uncommon words are better.',
|
154
|
+
'Use a longer keyboard pattern with more turns'
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'that contain a keyboard pattern with one turn' do
|
159
|
+
feedback = tester.test('zaqwer').feedback
|
160
|
+
|
161
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
162
|
+
expect(feedback.warning).to eql(
|
163
|
+
'Short keyboard patterns are easy to guess'
|
164
|
+
)
|
165
|
+
expect(feedback.suggestions).to contain_exactly(
|
166
|
+
'Add another word or two. Uncommon words are better.',
|
167
|
+
'Use a longer keyboard pattern with more turns'
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'for passwords with repeated characters' do
|
173
|
+
feedback = tester.test('zzz').feedback
|
174
|
+
|
175
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
176
|
+
expect(feedback.warning).to eql(
|
177
|
+
'Repeats like "aaa" are easy to guess'
|
178
|
+
)
|
179
|
+
expect(feedback.suggestions).to contain_exactly(
|
180
|
+
'Add another word or two. Uncommon words are better.',
|
181
|
+
'Avoid repeated words and characters'
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'for passwords with sequential characters' do
|
186
|
+
feedback = tester.test('pqrpqrpqr').feedback
|
187
|
+
|
188
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
189
|
+
expect(feedback.warning).to eql(
|
190
|
+
'Sequences like abc or 6543 are easy to guess'
|
191
|
+
)
|
192
|
+
expect(feedback.suggestions).to contain_exactly(
|
193
|
+
'Add another word or two. Uncommon words are better.',
|
194
|
+
'Avoid sequences'
|
195
|
+
)
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'for passwords containing dates' do
|
199
|
+
feedback = tester.test('testing02\12\1997').feedback
|
200
|
+
|
201
|
+
expect(feedback).to be_a Zxcvbn::Feedback
|
202
|
+
expect(feedback.warning).to eql(
|
203
|
+
'Dates are often easy to guess'
|
204
|
+
)
|
205
|
+
expect(feedback.suggestions).to contain_exactly(
|
206
|
+
'Add another word or two. Uncommon words are better.',
|
207
|
+
'Avoid dates and years that are associated with you'
|
208
|
+
)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|