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.
@@ -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