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