zxcvbn-ruby 0.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +12 -0
  3. data/CHANGELOG.md +42 -0
  4. data/CODE_OF_CONDUCT.md +130 -0
  5. data/Gemfile +8 -1
  6. data/Guardfile +26 -0
  7. data/{LICENSE → LICENSE.txt} +0 -0
  8. data/README.md +165 -9
  9. data/Rakefile +5 -1
  10. data/lib/zxcvbn.rb +10 -36
  11. data/lib/zxcvbn/crack_time.rb +44 -42
  12. data/lib/zxcvbn/data.rb +29 -0
  13. data/lib/zxcvbn/dictionary_ranker.rb +0 -2
  14. data/lib/zxcvbn/entropy.rb +3 -1
  15. data/lib/zxcvbn/feedback.rb +10 -0
  16. data/lib/zxcvbn/feedback_giver.rb +133 -0
  17. data/lib/zxcvbn/matchers/date.rb +2 -0
  18. data/lib/zxcvbn/matchers/dictionary.rb +2 -0
  19. data/lib/zxcvbn/matchers/digits.rb +2 -0
  20. data/lib/zxcvbn/matchers/l33t.rb +2 -2
  21. data/lib/zxcvbn/matchers/regex_helpers.rb +2 -0
  22. data/lib/zxcvbn/matchers/repeat.rb +2 -0
  23. data/lib/zxcvbn/matchers/sequences.rb +2 -0
  24. data/lib/zxcvbn/matchers/spatial.rb +2 -0
  25. data/lib/zxcvbn/matchers/year.rb +2 -0
  26. data/lib/zxcvbn/math.rb +2 -2
  27. data/lib/zxcvbn/omnimatch.rb +14 -3
  28. data/lib/zxcvbn/password_strength.rb +7 -3
  29. data/lib/zxcvbn/score.rb +1 -1
  30. data/lib/zxcvbn/scorer.rb +11 -0
  31. data/lib/zxcvbn/tester.rb +43 -0
  32. data/lib/zxcvbn/version.rb +1 -1
  33. data/spec/dictionary_ranker_spec.rb +2 -2
  34. data/spec/feedback_giver_spec.rb +212 -0
  35. data/spec/matchers/date_spec.rb +8 -8
  36. data/spec/matchers/dictionary_spec.rb +25 -14
  37. data/spec/matchers/digits_spec.rb +3 -3
  38. data/spec/matchers/l33t_spec.rb +15 -13
  39. data/spec/matchers/repeat_spec.rb +6 -6
  40. data/spec/matchers/sequences_spec.rb +5 -5
  41. data/spec/matchers/spatial_spec.rb +8 -8
  42. data/spec/matchers/year_spec.rb +3 -3
  43. data/spec/omnimatch_spec.rb +2 -2
  44. data/spec/scoring/crack_time_spec.rb +13 -13
  45. data/spec/scoring/entropy_spec.rb +28 -25
  46. data/spec/scoring/math_spec.rb +22 -18
  47. data/spec/support/matcher.rb +1 -1
  48. data/spec/tester_spec.rb +99 -0
  49. data/spec/zxcvbn_spec.rb +14 -39
  50. data/zxcvbn-ruby.gemspec +11 -0
  51. metadata +34 -29
@@ -1,5 +1,16 @@
1
+ require 'zxcvbn/entropy'
2
+ require 'zxcvbn/crack_time'
3
+ require 'zxcvbn/score'
4
+ require 'zxcvbn/match'
5
+
1
6
  module Zxcvbn
2
7
  class Scorer
8
+ def initialize(data)
9
+ @data = data
10
+ end
11
+
12
+ attr_reader :data
13
+
3
14
  include Entropy
4
15
  include CrackTime
5
16
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zxcvbn/data"
4
+ require "zxcvbn/password_strength"
5
+
6
+ module Zxcvbn
7
+ # Allows you to test the strength of multiple passwords without reading and
8
+ # parsing the dictionary data from disk each test. Dictionary data is read
9
+ # once from disk and stored in memory for the life of the Tester object.
10
+ #
11
+ # Example:
12
+ #
13
+ # tester = Zxcvbn::Tester.new
14
+ #
15
+ # tester.add_word_lists("custom" => ["words"])
16
+ #
17
+ # tester.test("password 1")
18
+ # tester.test("password 2")
19
+ # tester.test("password 3")
20
+ class Tester
21
+ def initialize
22
+ @data = Data.new
23
+ end
24
+
25
+ def test(password, user_inputs = [])
26
+ PasswordStrength.new(@data).test(password, sanitize(user_inputs))
27
+ end
28
+
29
+ def add_word_lists(lists)
30
+ lists.each_pair { |name, words| @data.add_word_list(name, sanitize(words)) }
31
+ end
32
+
33
+ def inspect
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) }
41
+ end
42
+ end
43
+ end
@@ -1,3 +1,3 @@
1
1
  module Zxcvbn
2
- VERSION = "0.0.3"
2
+ VERSION = '1.1.0'.freeze
3
3
  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
@@ -15,16 +15,16 @@ describe Zxcvbn::Matchers::Date do
15
15
  let(:matches) { matcher.matches(password) }
16
16
 
17
17
  it 'finds matches' do
18
- matches.should_not be_empty
18
+ expect(matches).not_to be_empty
19
19
  end
20
20
 
21
21
  it 'finds the correct matches' do
22
- matches.count.should eq 1
23
- matches[0].token.should eq %w[ 02 12 1997 ].join(separator)
24
- matches[0].separator.should eq separator
25
- matches[0].day.should eq 2
26
- matches[0].month.should eq 12
27
- matches[0].year.should eq 1997
22
+ expect(matches.count).to eq 1
23
+ expect(matches[0].token).to eq %w[ 02 12 1997 ].join(separator)
24
+ expect(matches[0].separator).to eq separator
25
+ expect(matches[0].day).to eq 2
26
+ expect(matches[0].month).to eq 12
27
+ expect(matches[0].year).to eq 1997
28
28
  end
29
29
  end
30
30
  end
@@ -103,7 +103,7 @@ describe Zxcvbn::Matchers::Date do
103
103
  let(:matches) { matcher.matches('testing0.x.1997') }
104
104
 
105
105
  it 'doesnt match' do
106
- matches.should be_empty
106
+ expect(matches).to be_empty
107
107
  end
108
108
  end
109
109
  end
@@ -1,19 +1,30 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
2
4
 
3
5
  describe Zxcvbn::Matchers::Dictionary do
4
- let(:matcher) { described_class.new('english', dictionary) }
5
- let(:dictionary) { Zxcvbn::RANKED_DICTIONARIES['english'] }
6
+ subject(:matcher) { described_class.new("Test dictionary", dictionary) }
6
7
 
7
- it 'finds all the matches' do
8
- matches = matcher.matches('whatisinit')
9
- matches.count.should == 14
10
- expected_matches = ['wha', 'what', 'ha', 'hat', 'a', 'at', 'tis', 'i', 'is',
11
- 'sin', 'i', 'in', 'i', 'it']
12
- matches.map(&:matched_word).should == expected_matches
13
- end
8
+ describe "#matches" do
9
+ let(:matches) { matcher.matches(password) }
10
+ let(:matched_words) { matches.map(&:matched_word) }
11
+
12
+ context "Given a dictionary of English words" do
13
+ let(:dictionary) { Zxcvbn::Data.new.ranked_dictionaries["english"] }
14
+ let(:password) { "whatisinit" }
15
+
16
+ it "finds all the matches" do
17
+ expect(matched_words).to match_array %w[wha what ha hat a at tis i is sin i in i it]
18
+ end
19
+ end
20
+
21
+ context "Given a custom dictionary" do
22
+ let(:dictionary) { Zxcvbn::DictionaryRanker.rank_dictionary(%w[test AB10CD]) }
23
+ let(:password) { "AB10CD" }
14
24
 
15
- it 'matches uppercase' do
16
- matcher = described_class.new('user_inputs', Zxcvbn::DictionaryRanker.rank_dictionary(['test','AB10CD']))
17
- matcher.matches('AB10CD').should_not be_empty
25
+ it "matches uppercase passwords with normalised dictionary entries" do
26
+ expect(matched_words).to match_array(%w[ab10cd])
27
+ end
28
+ end
18
29
  end
19
- end
30
+ end
@@ -5,11 +5,11 @@ describe Zxcvbn::Matchers::Digits do
5
5
  let(:matches) { matcher.matches('testing1239xx9712') }
6
6
 
7
7
  it 'sets the pattern name' do
8
- matches.all? { |m| m.pattern == 'digits' }.should eql(true)
8
+ expect(matches.all? { |m| m.pattern == 'digits' }).to eql(true)
9
9
  end
10
10
 
11
11
  it 'finds the correct matches' do
12
- matches.count.should == 2
13
- matches[0].token.should eq '1239'
12
+ expect(matches.count).to eq(2)
13
+ expect(matches[0].token).to eq '1239'
14
14
  end
15
15
  end
@@ -2,12 +2,12 @@ require 'spec_helper'
2
2
 
3
3
  describe Zxcvbn::Matchers::L33t do
4
4
  let(:matcher) { described_class.new([dictionary_matcher]) }
5
- let(:dictionary) { Zxcvbn::RANKED_DICTIONARIES['english'] }
5
+ let(:dictionary) { Zxcvbn::Data.new.ranked_dictionaries['english'] }
6
6
  let(:dictionary_matcher) { Zxcvbn::Matchers::Dictionary.new('english', dictionary) }
7
7
 
8
8
  describe '#relevant_l33t_substitutions' do
9
9
  it 'returns relevant l33t substitutions' do
10
- matcher.relevent_l33t_subtable('p@ssw1rd24').should eq(
10
+ expect(matcher.relevent_l33t_subtable('p@ssw1rd24')).to eq(
11
11
  {'a' => ['4', '@'], 'i' => ['1'], 'l' => ['1'], 'z' => ['2']}
12
12
  )
13
13
  end
@@ -17,14 +17,14 @@ describe Zxcvbn::Matchers::L33t do
17
17
  context 'with 2 possible substitutions' do
18
18
  it 'returns the correct possible substitutions' do
19
19
  substitutions = {'a' => ['@'], 'i' => ['1']}
20
- matcher.l33t_subs(substitutions).should match_array([
20
+ expect(matcher.l33t_subs(substitutions)).to match_array([
21
21
  {'@' => 'a', '1' => 'i'}
22
22
  ])
23
23
  end
24
24
 
25
25
  it 'returns the correct possible substitutions with multiple options' do
26
26
  substitutions = {'a' => ['@', '4'], 'i' => ['1']}
27
- matcher.l33t_subs(substitutions).should match_array([
27
+ expect(matcher.l33t_subs(substitutions)).to match_array([
28
28
  {'@' => 'a', '1' => 'i'},
29
29
  {'4' => 'a', '1' => 'i'}
30
30
  ])
@@ -34,7 +34,7 @@ describe Zxcvbn::Matchers::L33t do
34
34
  context 'with 3 possible substitutions' do
35
35
  it 'returns the correct possible substitutions' do
36
36
  substitutions = {'a' => ['@'], 'i' => ['1'], 'z' => ['3']}
37
- matcher.l33t_subs(substitutions).should match_array([
37
+ expect(matcher.l33t_subs(substitutions)).to match_array([
38
38
  {'@' => 'a', '1' => 'i', '3' => 'z'}
39
39
  ])
40
40
  end
@@ -43,7 +43,7 @@ describe Zxcvbn::Matchers::L33t do
43
43
  context 'with 4 possible substitutions' do
44
44
  it 'returns the correct possible substitutions' do
45
45
  substitutions = {'a' => ['@'], 'i' => ['1'], 'z' => ['3'], 'b' => ['8']}
46
- matcher.l33t_subs(substitutions).should match_array([
46
+ expect(matcher.l33t_subs(substitutions)).to match_array([
47
47
  {'@' => 'a', '1' => 'i', '3' => 'z', '8' => 'b'}
48
48
  ])
49
49
  end
@@ -51,12 +51,14 @@ describe Zxcvbn::Matchers::L33t do
51
51
  end
52
52
 
53
53
  describe '#matches' do
54
- let(:matches) { matcher.matches('p@ssword') }
55
- # it doesn't match on 'password' because that's not in the english
56
- # dictionary/frequency list
54
+ subject(:matches) { matcher.matches('p@ssword') }
55
+
56
+ it "doesn't find 'password' because it's not in english.txt" do
57
+ expect(matches.map(&:matched_word)).not_to include "password"
58
+ end
57
59
 
58
60
  it 'finds the correct matches' do
59
- matches.map(&:matched_word).should eq([
61
+ expect(matches.map(&:matched_word)).to match_array([
60
62
  'pas',
61
63
  'a',
62
64
  'as',
@@ -65,7 +67,7 @@ describe Zxcvbn::Matchers::L33t do
65
67
  end
66
68
 
67
69
  it 'sets the token correctly on those matches' do
68
- matches.map(&:token).should eq([
70
+ expect(matches.map(&:token)).to match_array([
69
71
  'p@s',
70
72
  '@',
71
73
  '@s',
@@ -74,7 +76,7 @@ describe Zxcvbn::Matchers::L33t do
74
76
  end
75
77
 
76
78
  it 'sets the substituions used' do
77
- matches.map(&:sub).should eq([
79
+ expect(matches.map(&:sub)).to match_array([
78
80
  {'@' => 'a'},
79
81
  {'@' => 'a'},
80
82
  {'@' => 'a'},
@@ -82,4 +84,4 @@ describe Zxcvbn::Matchers::L33t do
82
84
  ])
83
85
  end
84
86
  end
85
- end
87
+ end