zxcvbn-ruby 0.0.1

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 (57) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +23 -0
  6. data/Rakefile +18 -0
  7. data/data/adjacency_graphs.json +9 -0
  8. data/data/frequency_lists.yaml +85094 -0
  9. data/lib/zxcvbn.rb +37 -0
  10. data/lib/zxcvbn/crack_time.rb +51 -0
  11. data/lib/zxcvbn/dictionary_ranker.rb +23 -0
  12. data/lib/zxcvbn/entropy.rb +151 -0
  13. data/lib/zxcvbn/match.rb +13 -0
  14. data/lib/zxcvbn/matchers/date.rb +134 -0
  15. data/lib/zxcvbn/matchers/dictionary.rb +34 -0
  16. data/lib/zxcvbn/matchers/digits.rb +18 -0
  17. data/lib/zxcvbn/matchers/l33t.rb +127 -0
  18. data/lib/zxcvbn/matchers/new_l33t.rb +120 -0
  19. data/lib/zxcvbn/matchers/regex_helpers.rb +21 -0
  20. data/lib/zxcvbn/matchers/repeat.rb +32 -0
  21. data/lib/zxcvbn/matchers/sequences.rb +64 -0
  22. data/lib/zxcvbn/matchers/spatial.rb +79 -0
  23. data/lib/zxcvbn/matchers/year.rb +18 -0
  24. data/lib/zxcvbn/math.rb +63 -0
  25. data/lib/zxcvbn/omnimatch.rb +49 -0
  26. data/lib/zxcvbn/password_strength.rb +21 -0
  27. data/lib/zxcvbn/score.rb +15 -0
  28. data/lib/zxcvbn/scorer.rb +84 -0
  29. data/lib/zxcvbn/version.rb +3 -0
  30. data/spec/matchers/date_spec.rb +109 -0
  31. data/spec/matchers/dictionary_spec.rb +14 -0
  32. data/spec/matchers/digits_spec.rb +15 -0
  33. data/spec/matchers/l33t_spec.rb +85 -0
  34. data/spec/matchers/repeat_spec.rb +18 -0
  35. data/spec/matchers/sequences_spec.rb +16 -0
  36. data/spec/matchers/spatial_spec.rb +20 -0
  37. data/spec/matchers/year_spec.rb +15 -0
  38. data/spec/omnimatch_spec.rb +24 -0
  39. data/spec/scorer_spec.rb +5 -0
  40. data/spec/scoring/crack_time_spec.rb +106 -0
  41. data/spec/scoring/entropy_spec.rb +213 -0
  42. data/spec/scoring/math_spec.rb +131 -0
  43. data/spec/spec_helper.rb +54 -0
  44. data/spec/support/js_helpers.rb +35 -0
  45. data/spec/support/js_source/adjacency_graphs.js +8 -0
  46. data/spec/support/js_source/compiled.js +1188 -0
  47. data/spec/support/js_source/frequency_lists.js +10 -0
  48. data/spec/support/js_source/init.coffee +63 -0
  49. data/spec/support/js_source/init.js +95 -0
  50. data/spec/support/js_source/matching.coffee +444 -0
  51. data/spec/support/js_source/matching.js +685 -0
  52. data/spec/support/js_source/scoring.coffee +270 -0
  53. data/spec/support/js_source/scoring.js +390 -0
  54. data/spec/support/matcher.rb +35 -0
  55. data/spec/zxcvbn_spec.rb +49 -0
  56. data/zxcvbn-ruby.gemspec +20 -0
  57. metadata +167 -0
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Matchers::Repeat do
4
+ let(:matcher) { subject }
5
+ let(:matches) { matcher.matches('bbbbbtestingaaa') }
6
+
7
+ it 'sets the pattern name' do
8
+ matches.all? { |m| m.pattern == 'repeat' }.should be_true
9
+ end
10
+
11
+ it 'finds the repeated patterns' do
12
+ matches.count.should eq 2
13
+ matches[0].token.should eq 'bbbbb'
14
+ matches[0].repeated_char.should eq 'b'
15
+ matches[1].token.should eq 'aaa'
16
+ matches[1].repeated_char.should eq 'a'
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Matchers::Sequences do
4
+ let(:matcher) { subject }
5
+ let(:matches) { matcher.matches('abcde87654') }
6
+
7
+ it 'sets the pattern name' do
8
+ matches.all? { |m| m.pattern == 'sequence' }.should be_true
9
+ end
10
+
11
+ it 'finds the correct matches' do
12
+ matches.count.should == 2
13
+ matches[0].token.should eq 'abcde'
14
+ matches[1].token.should eq '87654'
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Matchers::Spatial do
4
+ let(:matcher) { Zxcvbn::Matchers::Spatial.new(graphs) }
5
+ let(:graphs) { Zxcvbn::ADJACENCY_GRAPHS }
6
+
7
+ describe '#matches' do
8
+ let(:matches) { matcher.matches('rtyikm') }
9
+
10
+ it 'finds the correct of matches' do
11
+ matches.count.should eq 3
12
+ matches[0].token.should eq 'rty'
13
+ matches[0].graph.should eq 'qwerty'
14
+ matches[1].token.should eq 'ikm'
15
+ matches[1].graph.should eq 'qwerty'
16
+ matches[2].token.should eq 'yik'
17
+ matches[2].graph.should eq 'dvorak'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Matchers::Year do
4
+ let(:matcher) { subject }
5
+ let(:matches) { matcher.matches('testing1998') }
6
+
7
+ it 'sets the pattern name' do
8
+ matches.all? { |m| m.pattern == 'year' }.should be_true
9
+ end
10
+
11
+ it 'finds the correct matches' do
12
+ matches.count.should == 1
13
+ matches[0].token.should eq '1998'
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Omnimatch do
4
+ before(:all) do
5
+ @omnimatch = described_class.new
6
+ end
7
+
8
+ def omnimatch(password)
9
+ @omnimatch.matches(password)
10
+ end
11
+
12
+ def js_omnimatch(password)
13
+ run_js(%'omnimatch("#{password}")')
14
+ end
15
+
16
+ TEST_PASSWORDS.each do |password|
17
+ it "gives back the same results for #{password}" do
18
+ js_results = js_omnimatch(password)
19
+ ruby_results = omnimatch(password)
20
+
21
+ ruby_results.should match_js_results js_results
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Scorer do
4
+
5
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::CrackTime do
4
+ include Zxcvbn::CrackTime
5
+
6
+ describe '#entropy_to_crack_time' do
7
+ specify do
8
+ entropy_to_crack_time(15.433976574415976).should eq 2.2134000000000014
9
+ end
10
+ end
11
+
12
+ describe '#crack_time_to_score' do
13
+ context 'crack time less than 10 to the power 2' do
14
+ it 'returns 0' do
15
+ crack_time_to_score(90).should eq 0
16
+ end
17
+ end
18
+
19
+ context 'crack time in between 10**2 and 10**4' do
20
+ it 'returns 1' do
21
+ crack_time_to_score(5000).should eq 1
22
+ end
23
+ end
24
+
25
+ context 'crack time in between 10**4 and 10**6' do
26
+ it 'returns 2' do
27
+ crack_time_to_score(500_000).should eq 2
28
+ end
29
+ end
30
+
31
+ context 'crack time in between 10**6 and 10**8' do
32
+ it 'returns 3' do
33
+ crack_time_to_score(50_000_000).should eq 3
34
+ end
35
+ end
36
+
37
+ context 'crack time above 10**8' do
38
+ it 'returns 4' do
39
+ crack_time_to_score(110_000_000).should eq 4
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '#display_time' do
45
+ let(:minute_to_seconds) { 60 }
46
+ let(:hour_to_seconds) { minute_to_seconds * 60 }
47
+ let(:day_to_seconds) { hour_to_seconds * 24 }
48
+ let(:month_to_seconds) { day_to_seconds * 31 }
49
+ let(:year_to_seconds) { month_to_seconds * 12 }
50
+ let(:century_to_seconds) { year_to_seconds * 100 }
51
+
52
+ context 'when less than a minute' do
53
+ it 'should return instant' do
54
+ [0, minute_to_seconds - 1].each do |seconds|
55
+ display_time(seconds).should eql 'instant'
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'when less than an hour' do
61
+ it 'should return a readable time in minutes' do
62
+ [60, (hour_to_seconds - 1)].each do |seconds|
63
+ display_time(seconds).should =~ /[0-9]+ minutes$/
64
+ end
65
+ end
66
+ end
67
+
68
+ context 'when less than a day' do
69
+ it 'should return a readable time in hours' do
70
+ [hour_to_seconds, (day_to_seconds - 1)].each do |seconds|
71
+ display_time(seconds).should =~ /[0-9]+ hours$/
72
+ end
73
+ end
74
+ end
75
+
76
+ context 'when less than 31 days' do
77
+ it 'should return a readable time in days' do
78
+ [day_to_seconds, month_to_seconds - 1].each do |seconds|
79
+ display_time(seconds).should =~ /[0-9]+ days$/
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'when less than 1 year' do
85
+ it 'should return a readable time in days' do
86
+ [month_to_seconds, (year_to_seconds - 1)].each do |seconds|
87
+ display_time(seconds).should =~ /[0-9]+ months$/
88
+ end
89
+ end
90
+ end
91
+
92
+ context 'when less than a century' do
93
+ it 'should return a readable time in days' do
94
+ [year_to_seconds, (century_to_seconds - 1)].each do |seconds|
95
+ display_time(seconds).should =~ /[0-9]+ years$/
96
+ end
97
+ end
98
+ end
99
+
100
+ context 'when a century or more' do
101
+ it 'should return centuries' do
102
+ display_time(century_to_seconds).should eql 'centuries'
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,213 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Entropy do
4
+ include Zxcvbn::Math
5
+
6
+ let(:entropy) {
7
+ Class.new do
8
+ include Zxcvbn::Entropy
9
+ end.new
10
+ }
11
+
12
+ describe '#repeat_entropy' do
13
+ it 'returns the correct value' do
14
+ match = Zxcvbn::Match.new(:token => '2222')
15
+ entropy.repeat_entropy(match).should eq 5.321928094887363
16
+ end
17
+ end
18
+
19
+ describe '#sequence_entropy' do
20
+ let(:match) { Zxcvbn::Match.new(:token => token, :ascending => true) }
21
+
22
+ {'a' => 'abcdefg', '1' => '1234567'}.each do |first_char, token|
23
+ context "when the first char is #{first_char}" do
24
+ let(:token) { token }
25
+
26
+ it 'returns the correct value' do
27
+ entropy.sequence_entropy(match).should eq 3.807354922057604
28
+ end
29
+ end
30
+ end
31
+
32
+ context 'when the first character is a digit' do
33
+ let(:token) { '23456' }
34
+
35
+ it 'returns the correct value' do
36
+ entropy.sequence_entropy(match).should eq 5.643856189774725
37
+ end
38
+ end
39
+
40
+ context 'when the first character is a lowercase letter' do
41
+ let(:token) { 'bcdef' }
42
+
43
+ it 'returns the correct value' do
44
+ entropy.sequence_entropy(match).should eq 7.022367813028454
45
+ end
46
+ end
47
+
48
+ context 'when the first character is an uppercase letter' do
49
+ let(:token) { 'BCDEF' }
50
+
51
+ it 'returns the correct value' do
52
+ entropy.sequence_entropy(match).should eq 8.022367813028454
53
+ end
54
+ end
55
+
56
+ context 'when the match is ascending' do
57
+ before { match.ascending = false }
58
+ let(:token) { 'bcdef' }
59
+
60
+ it 'returns the correct value' do
61
+ entropy.sequence_entropy(match).should eq 8.022367813028454
62
+ end
63
+ end
64
+ end
65
+
66
+ describe '#digits_entropy' do
67
+ it 'returns the correct value' do
68
+ match = Zxcvbn::Match.new(:token => '12345678')
69
+ entropy.digits_entropy(match).should eq 26.5754247590989
70
+ end
71
+ end
72
+
73
+ describe '#year_entropy' do
74
+ it 'returns the correct value' do
75
+ entropy.year_entropy(nil).should eq 6.894817763307944
76
+ end
77
+ end
78
+
79
+ describe '#date_entropy' do
80
+ context 'with a two digit year' do
81
+ it 'returns the correct value' do
82
+ match = Zxcvbn::Match.new(:year => 98)
83
+ entropy.date_entropy(match).should eq 15.183015000882756
84
+ end
85
+ end
86
+
87
+ context 'with a four digit year' do
88
+ it 'returns the correct value' do
89
+ match = Zxcvbn::Match.new(:year => 2012)
90
+ entropy.date_entropy(match).should eq 15.433976574415976
91
+ end
92
+ end
93
+
94
+ context 'with a separator' do
95
+ it 'returns the correct value' do
96
+ match = Zxcvbn::Match.new(:year => 2012, :separator => '/')
97
+ entropy.date_entropy(match).should eq 17.433976574415976
98
+ end
99
+ end
100
+ end
101
+
102
+ describe '#dictionary_entropy' do
103
+ let(:match) { Zxcvbn::Match.new(:token => token, :rank => rank, :l33t => l33t, :sub => sub) }
104
+ let(:l33t) { false }
105
+ let(:sub) { {} }
106
+ let(:calculated_entropy) { entropy.dictionary_entropy(match) }
107
+
108
+ context 'a simple dictionary word, all lower case and no l33t subs' do
109
+ let(:token) { 'you' }
110
+ let(:rank) { 1 }
111
+
112
+ specify { calculated_entropy.should eq 0 }
113
+ end
114
+
115
+ context 'with all upper case characters' do
116
+ let(:token) { 'YOU' }
117
+ let(:rank) { 1 }
118
+
119
+ specify { calculated_entropy.should eq 1 }
120
+ end
121
+
122
+ context 'starting with uppercase' do
123
+ let(:token) { 'You' }
124
+ let(:rank) { 1 }
125
+
126
+ specify { calculated_entropy.should eq 1 }
127
+ end
128
+
129
+ context 'starting with uppercase' do
130
+ let(:token) { 'yoU' }
131
+ let(:rank) { 1 }
132
+
133
+ specify { calculated_entropy.should eq 1 }
134
+ end
135
+
136
+ context 'mixed upper and lower' do
137
+ let(:token) { 'tEsTiNg' }
138
+ let(:rank) { 1 }
139
+
140
+ specify { calculated_entropy.should eq 6 }
141
+ end
142
+
143
+ context 'starting with digits' do
144
+ let(:token) { '12345' }
145
+ let(:rank) { 1 }
146
+
147
+ specify { calculated_entropy.should eq 0 }
148
+ end
149
+
150
+ context 'extra l33t entropy' do
151
+ let(:token) { 'p3rs0n' }
152
+ let(:rank) { 1 }
153
+ let(:l33t) { true }
154
+ let(:sub) { {'3' => 'e', '0' => 'o'} }
155
+
156
+ specify { calculated_entropy.should eq 1 }
157
+ end
158
+ end
159
+
160
+ describe '#spatial_entropy' do
161
+ let(:match) { Zxcvbn::Match.new(:token => '123wsclf', :turns => 1) }
162
+
163
+ context 'when keyboard is qwerty' do
164
+ it 'should return the correct entropy' do
165
+ match.graph = 'qwerty'
166
+
167
+ entropy.spatial_entropy(match).should eql 11.562242424221074
168
+ end
169
+ end
170
+
171
+ context 'when keyboard is dvorak' do
172
+ it 'should return the correct entropy' do
173
+ match.graph = 'dvorak'
174
+
175
+ entropy.spatial_entropy(match).should eql 11.562242424221074
176
+ end
177
+ end
178
+
179
+ context 'when keyboard is not qwerty or dvorak' do
180
+ it 'should return the correct entropy' do
181
+ match.graph = 'keypad'
182
+
183
+ entropy.spatial_entropy(match).should eql 9.05528243550119
184
+ end
185
+ end
186
+
187
+ context 'when match includes several turns' do
188
+ it 'should return the correct entropy' do
189
+ match.turns = 5
190
+
191
+ entropy.spatial_entropy(match).should eql 21.761397858718993
192
+ end
193
+ end
194
+
195
+ context 'when match includes shifted count' do
196
+ it 'should return the correct entropy' do
197
+ match.shiffted_count = 5
198
+
199
+ entropy.spatial_entropy(match).should eql 9.05528243550119
200
+ end
201
+ end
202
+
203
+ context 'when match includes shifted count and several turns' do
204
+ it 'should return the correct entropy' do
205
+ match.shiffted_count = 5
206
+ match.turns = 5
207
+
208
+ entropy.spatial_entropy(match).should eql 21.761397858718993
209
+ end
210
+ end
211
+ end
212
+
213
+ end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+
3
+ describe Zxcvbn::Math do
4
+ include Zxcvbn::Math
5
+
6
+ describe '#bruteforce_cardinality' do
7
+ context 'when empty password' do
8
+ it 'should return 0 if empty password' do
9
+ bruteforce_cardinality('').should eql 0
10
+ end
11
+ end
12
+
13
+ context 'when password is one character long' do
14
+ context 'and a digit' do
15
+ it 'should return 10' do
16
+ (0..9).each do |digit|
17
+ bruteforce_cardinality(digit.to_s).should eql 10
18
+ end
19
+ end
20
+ end
21
+
22
+ context 'and an upper case character' do
23
+ it 'should return 26' do
24
+ ('A'..'Z').each do |character|
25
+ bruteforce_cardinality(character).should eql 26
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'and a lower case character' do
31
+ it 'should return 26' do
32
+ ('a'..'z').each do |character|
33
+ bruteforce_cardinality(character).should eql 26
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'and a symbol' do
39
+ it 'should return 33' do
40
+ %w|/ [ ` {|.each do |symbol|
41
+ bruteforce_cardinality(symbol).should eql 33
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'when password is more than one character long' do
48
+ context 'and only digits' do
49
+ it 'should return 10' do
50
+ bruteforce_cardinality('123456789').should eql 10
51
+ end
52
+ end
53
+
54
+ context 'and only lowercase characters' do
55
+ it 'should return 26' do
56
+ bruteforce_cardinality('password').should eql 26
57
+ end
58
+ end
59
+
60
+ context 'and only uppercase characters' do
61
+ it 'should return 26' do
62
+ bruteforce_cardinality('PASSWORD').should eql 26
63
+ end
64
+ end
65
+
66
+ context 'and only symbols' do
67
+ it 'should return 33' do
68
+ bruteforce_cardinality('/ [ ` {').should eql 33
69
+ end
70
+ end
71
+
72
+ context 'and a mixed of character types' do
73
+ it 'should add up every character type cardinality' do
74
+ bruteforce_cardinality('p1SsWorD!').should eql 95
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#average_degree_for_graph' do
81
+ context 'when keyboard is qwerty' do
82
+ it 'returns the correct average degree over all keys' do
83
+ average_degree_for_graph('qwerty').should eql 4.595744680851064
84
+ end
85
+ end
86
+
87
+ context 'when keyboard is dvorak' do
88
+ it 'returns the correct average degree over all keys' do
89
+ average_degree_for_graph('dvorak').should eql 4.595744680851064
90
+ end
91
+ end
92
+
93
+ context 'when keyboard is keypad' do
94
+ it 'returns the correct average degree over all keys' do
95
+ average_degree_for_graph('keypad').should eql 5.066666666666666
96
+ end
97
+ end
98
+
99
+ context 'when keyboard is mac keypad' do
100
+ it 'returns the correct average degree over all keys' do
101
+ average_degree_for_graph('mac_keypad').should eql 5.25
102
+ end
103
+ end
104
+ end
105
+
106
+ describe '#starting_positions_for_graph' do
107
+ context 'when keyboard is qwerty' do
108
+ it 'returns the correct average degree over all keys' do
109
+ starting_positions_for_graph('qwerty').should eql 94
110
+ end
111
+ end
112
+
113
+ context 'when keyboard is dvorak' do
114
+ it 'returns the correct average degree over all keys' do
115
+ starting_positions_for_graph('dvorak').should eql 94
116
+ end
117
+ end
118
+
119
+ context 'when keyboard is keypad' do
120
+ it 'returns the correct average degree over all keys' do
121
+ starting_positions_for_graph('keypad').should eql 15
122
+ end
123
+ end
124
+
125
+ context 'when keyboard is mac keypad' do
126
+ it 'returns the correct average degree over all keys' do
127
+ starting_positions_for_graph('mac_keypad').should eql 16
128
+ end
129
+ end
130
+ end
131
+ end