zxcvbn-ruby 0.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ module Zxcvbn
2
+ class Feedback
3
+ attr_accessor :warning, :suggestions
4
+
5
+ def initialize(options = {})
6
+ @warning = options[:warning]
7
+ @suggestions = options[:suggestions] || []
8
+ end
9
+ end
10
+ 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
@@ -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 |substitution, letter|
35
- match_substitutions[substitution] = letter if token.include?(substitution)
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
@@ -1,7 +1,7 @@
1
1
  module Zxcvbn
2
2
  class Score
3
3
  attr_accessor :entropy, :crack_time, :crack_time_display, :score, :pattern,
4
- :match_sequence, :password, :calc_time
4
+ :match_sequence, :password, :calc_time, :feedback
5
5
 
6
6
  def initialize(options = {})
7
7
  @entropy = options[:entropy]
@@ -1,5 +1,7 @@
1
- require 'zxcvbn/data'
2
- require 'zxcvbn/password_strength'
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#{self.__id__.to_s(16)}>"
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zxcvbn
2
- VERSION = "0.1.0"
4
+ VERSION = '1.2.0'
3
5
  end
@@ -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].should eq({'abc' => 1, 'def' => 2})
9
- result[:test2].should eq({'def' => 1, 'abc' => 2})
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