password_strength 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = Changelog
2
+
3
+ == March 5 2010
4
+
5
+ * Released 0.1.0 version
data/README.rdoc ADDED
@@ -0,0 +1,97 @@
1
+ = Introduction
2
+
3
+ Validates the strength of a password according to several rules:
4
+
5
+ * size
6
+ * 3+ numbers
7
+ * 2+ special characters
8
+ * uppercased and downcased letters
9
+ * combination of numbers, letters and symbols
10
+ * password contains username
11
+ * sequences (123, abc, aaa)
12
+
13
+ Some results:
14
+
15
+ * <tt>123</tt>: weak
16
+ * <tt>123abc</tt>: weak
17
+ * <tt>aaaaaa</tt>: weak
18
+ * <tt>myPass145</tt>: good
19
+ * <tt>myPass145$</tt>: strong
20
+
21
+ = Install
22
+
23
+ sudo gem install password_strength
24
+
25
+ If you want the source go to http://github.com/fnando/password_strength
26
+
27
+ = Usage
28
+
29
+ strength = PasswordStrength.test("johndoe", "mypass")
30
+ #=> return a object
31
+
32
+ strength.good?
33
+ #=> status == :good
34
+
35
+ strength.weak?
36
+ #=> status == :weak
37
+
38
+ strength.strong?
39
+ #=> status == :strong
40
+
41
+ strength.status
42
+ #=> can be :weak, :good, :strong
43
+
44
+ strength.valid?(:strong)
45
+ #=> strength == :strong
46
+
47
+ strength.valid?(:good)
48
+ #=> strength == :good or strength == :strong
49
+
50
+ = ActiveRecord
51
+
52
+ The PasswordStrength library comes with ActiveRecord support (tested on AR 2.3.5 and 3.0.0-beta).
53
+
54
+ class Person < ActiveRecord::Base
55
+ validates_strength_of :password
56
+ end
57
+
58
+ The default options are <tt>:level => :good, :with => :username</tt>.
59
+
60
+ If you want to compare your password against other field, you have to set the <tt>:with</tt> option.
61
+
62
+ validates_strength_of :password, :with => :email
63
+
64
+ The available levels are: <tt>:weak</tt>, <tt>:good</tt> and <tt>:strong</tt>
65
+
66
+ = JavaScript
67
+
68
+ The PasswordStrength also implements the algorithm in the JavaScript.
69
+
70
+ var strength = PasswordStrength.test("johndoe", "mypass");
71
+ strength.isGood();
72
+ strength.isStrong();
73
+ strength.isWeak();
74
+ strength.isValid("good");
75
+
76
+ The API is basically the same!
77
+
78
+ Get the file: http://github.com/fnando/password_strength/raw/master/lib/password_strength.js
79
+
80
+ = TO-DO
81
+
82
+ * Detect repetitions
83
+ * Rake task to get the latest JavaScript file
84
+
85
+ = License
86
+
87
+ (The MIT License)
88
+
89
+ Copyright © 2010:
90
+
91
+ * Nando Vieira (http://simplesideias.com.br)
92
+
93
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
94
+
95
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
96
+
97
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,194 @@
1
+ var PasswordStrength = function() {
2
+ var MULTIPLE_NUMBERS_RE = /\d.*?\d.*?\d/;
3
+ var MULTIPLE_SYMBOLS_RE = /[!@#$%^&*?_~].*?[!@#$%^&*?_~]/;
4
+ var UPPERCASE_LOWERCASE_RE = /([a-z].*[A-Z])|([A-Z].*[a-z])/;
5
+ var SYMBOL_RE = /[!@#\$%^&*?_~]/;
6
+
7
+ this.username = null;
8
+ this.password = null;
9
+ this.score = 0;
10
+ this.status = null;
11
+
12
+ this.test = function() {
13
+ this.score = 0;
14
+ this.score += this.scoreFor("password_size");
15
+ this.score += this.scoreFor("numbers");
16
+ this.score += this.scoreFor("symbols");
17
+ this.score += this.scoreFor("uppercase_lowercase");
18
+ this.score += this.scoreFor("numbers_chars");
19
+ this.score += this.scoreFor("numbers_symbols");
20
+ this.score += this.scoreFor("symbols_chars");
21
+ this.score += this.scoreFor("only_chars");
22
+ this.score += this.scoreFor("only_numbers");
23
+ this.score += this.scoreFor("username");
24
+ this.score += this.scoreFor("sequences");
25
+
26
+ if (this.score < 0) {
27
+ this.score = 0;
28
+ }
29
+
30
+ if (this.score > 100) {
31
+ this.score = 100;
32
+ }
33
+
34
+ if (this.score < 35) {
35
+ this.status = "weak";
36
+ }
37
+
38
+ if (this.score >= 35 && this.score < 70) {
39
+ this.status = "good";
40
+ }
41
+
42
+ if (this.score >= 70) {
43
+ this.status = "strong";
44
+ }
45
+
46
+ return this.score;
47
+ };
48
+
49
+ this.scoreFor = function(name) {
50
+ score = 0;
51
+
52
+ switch (name) {
53
+ case "password_size":
54
+ if (this.password.length < 4) {
55
+ score = -100;
56
+ } else {
57
+ score = this.password.length * 4;
58
+ }
59
+ break;
60
+
61
+ case "numbers":
62
+ if (this.password.match(MULTIPLE_NUMBERS_RE)) {
63
+ score = 5;
64
+ }
65
+ break;
66
+
67
+ case "symbols":
68
+ if (this.password.match(MULTIPLE_SYMBOLS_RE)) {
69
+ score = 5;
70
+ }
71
+ break;
72
+
73
+ case "uppercase_lowercase":
74
+ if (this.password.match(UPPERCASE_LOWERCASE_RE)) {
75
+ score = 10;
76
+ }
77
+ break;
78
+
79
+ case "numbers_chars":
80
+ if (this.password.match(/[a-z]/i) && this.password.match(/[0-9]/)) {
81
+ score = 15;
82
+ }
83
+ break;
84
+
85
+ case "numbers_symbols":
86
+ if (this.password.match(/[0-9]/) && this.password.match(SYMBOL_RE)) {
87
+ score = 15;
88
+ }
89
+ break;
90
+
91
+ case "symbols_chars":
92
+ if (this.password.match(/[a-z]/i) && this.password.match(SYMBOL_RE)) {
93
+ score = 15;
94
+ }
95
+ break;
96
+
97
+ case "only_chars":
98
+ if (this.password.match(/^[a-z]+$/i)) {
99
+ score = -15;
100
+ }
101
+ break;
102
+
103
+ case "only_numbers":
104
+ if (this.password.match(/^\d+$/i)) {
105
+ score = -15;
106
+ }
107
+ break;
108
+
109
+ case "username":
110
+ if (this.password == this.username) {
111
+ score = -100;
112
+ } else if (this.password.indexOf(this.username) != -1) {
113
+ score = -15;
114
+ }
115
+ break;
116
+
117
+ case "sequences":
118
+ score += -15 * this.detectSequences(this.password);
119
+ score += -15 * this.detectSequences(this.reversed(this.password));
120
+ break
121
+ };
122
+
123
+ return score;
124
+ };
125
+
126
+ this.isGood = function() {
127
+ return this.status == "good";
128
+ };
129
+
130
+ this.isWeak = function() {
131
+ return this.status == "weak";
132
+ };
133
+
134
+ this.isStrong = function() {
135
+ return this.status == "strong";
136
+ };
137
+
138
+ this.isValid = function(level) {
139
+ if(level == "strong") {
140
+ return this.isStrong();
141
+ } else if (level == "good") {
142
+ return this.isStrong() || this.isGood();
143
+ } else {
144
+ return true;
145
+ }
146
+ };
147
+
148
+ this.detectSequences = function(text) {
149
+ var matches = 0;
150
+ var sequenceSize = 0;
151
+ var codes = [];
152
+ var len = text.length;
153
+ var previousCode, currentCode;
154
+
155
+ for (var i = 0; i < len; i++) {
156
+ currentCode = text.charCodeAt(i);
157
+ previousCode = codes[codes.length - 1];
158
+ codes.push(currentCode);
159
+
160
+ if (previousCode) {
161
+ if (currentCode == previousCode + 1 || previousCode == currentCode) {
162
+ sequenceSize += 1;
163
+ } else {
164
+ sequenceSize = 0;
165
+ }
166
+ }
167
+
168
+ if (sequenceSize == 2) {
169
+ matches += 1;
170
+ }
171
+ }
172
+
173
+ return matches;
174
+ };
175
+
176
+ this.reversed = function(text) {
177
+ var newText = "";
178
+ var len = text.length;
179
+
180
+ for (var i = len -1; i >= 0; i--) {
181
+ newText += text.charAt(i);
182
+ }
183
+
184
+ return newText;
185
+ };
186
+ };
187
+
188
+ PasswordStrength.test = function(username, password) {
189
+ strength = new PasswordStrength();
190
+ strength.username = username;
191
+ strength.password = password;
192
+ strength.test();
193
+ return strength;
194
+ };
@@ -0,0 +1,17 @@
1
+ require "password_strength/base"
2
+ require "password_strength/active_record"
3
+
4
+ module PasswordStrength
5
+ # Test the password strength by applying several rules.
6
+ # The username is required to match its substring in passwords.
7
+ #
8
+ # strength = PasswordStrength.test("johndoe", "mypass")
9
+ # strength.weak?
10
+ # #=> true
11
+ #
12
+ def self.test(username, password)
13
+ strength = Base.new(username, password)
14
+ strength.test
15
+ strength
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ if defined?(Rails)
2
+ if Rails.version >= "3"
3
+ require "active_record"
4
+ require "password_strength/active_record/ar3"
5
+ else
6
+ require "password_strength/active_record/ar2"
7
+ end
8
+ end
@@ -0,0 +1,34 @@
1
+ module PasswordStrength
2
+ module ActiveRecord
3
+ # Validates that the specified attributes are not weak (according to several rules).
4
+ #
5
+ # class Person < ActiveRecord::Base
6
+ # validates_strength_of :password
7
+ # end
8
+ #
9
+ # The default options are <tt>:level => :good, :with => :username</tt>.
10
+ #
11
+ # If you want to compare your password against other field, you have to set the <tt>:with</tt> option.
12
+ #
13
+ # validates_strength_of :password, :with => :email
14
+ #
15
+ # The available levels are: <tt>:weak</tt>, <tt>:good</tt> and <tt>:strong</tt>
16
+ #
17
+ def validates_strength_of(*attr_names)
18
+ options = attr_names.extract_options!
19
+ options.reverse_merge!(:level => :good, :with => :username, :message => "is too weak")
20
+
21
+ raise ArgumentError, "The :with option must be supplied" unless options.include?(:with)
22
+ raise ArgumentError, "The :level option must be one of [:weak, :good, :strong]" unless [:weak, :good, :strong].include?(options[:level])
23
+
24
+ validates_each(attr_names, options) do |record, attr_name, value|
25
+ strength = PasswordStrength.test(record.send(options[:with]), value)
26
+ record.errors.add(attr_name, :too_weak, :default => options[:message]) unless strength.valid?(options[:level])
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ class ActiveRecord::Base # :nodoc:
33
+ extend PasswordStrength::ActiveRecord
34
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveModel # :nodoc:
2
+ module Validations # :nodoc:
3
+ class StrengthValidator < EachValidator # :nodoc: all
4
+ def initialize(options)
5
+ super(options.reverse_merge(:level => :good, :with => :username, :message => "is too weak"))
6
+ end
7
+
8
+ def validate_each(record, attribute, value)
9
+ strength = PasswordStrength.test(record.send(options[:with]), value)
10
+ record.errors.add(attribute, :too_weak, :default => options[:message], :value => value) unless strength.valid?(options[:level])
11
+ end
12
+
13
+ def check_validity!
14
+ raise ArgumentError, "The :with option must be supplied" unless options.include?(:with)
15
+ raise ArgumentError, "The :level option must be one of [:weak, :good, :strong]" unless [:weak, :good, :strong].include?(options[:level])
16
+ super
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ # Validates that the specified attributes are not weak (according to several rules).
22
+ #
23
+ # class Person < ActiveRecord::Base
24
+ # validates_strength_of :password
25
+ # end
26
+ #
27
+ # The default options are <tt>:level => :good, :with => :username</tt>.
28
+ #
29
+ # If you want to compare your password against other field, you have to set the <tt>:with</tt> option.
30
+ #
31
+ # validates_strength_of :password, :with => :email
32
+ #
33
+ # The available levels are: <tt>:weak</tt>, <tt>:good</tt> and <tt>:strong</tt>
34
+ #
35
+ def validates_strength_of(*attr_names)
36
+ validates_with StrengthValidator, _merge_attributes(attr_names)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,153 @@
1
+ module PasswordStrength
2
+ class Base
3
+ MULTIPLE_NUMBERS_RE = /\d.*?\d.*?\d/
4
+ MULTIPLE_SYMBOLS_RE = /[!@#\$%^&*?_~-].*?[!@#\$%^&*?_~-]/
5
+ SYMBOL_RE = /[!@#\$%^&*?_~-]/
6
+ UPPERCASE_LOWERCASE_RE = /([a-z].*[A-Z])|([A-Z].*[a-z])/
7
+
8
+ # Hold the username that will be matched against password
9
+ attr_accessor :username
10
+
11
+ # The password that will be tested
12
+ attr_accessor :password
13
+
14
+ # The score for the latest test. Will be nil if the password has not been tested.
15
+ attr_reader :score
16
+
17
+ # The current test status. Can be +:weak+, +:good+ or +:strong+
18
+ attr_reader :status
19
+
20
+ def initialize(username, password)
21
+ @username = username.to_s
22
+ @password = password.to_s
23
+ @score = 0
24
+ end
25
+
26
+ # Check if the password has the specified score.
27
+ # Level can be +:weak+, +:good+ or +:strong+.
28
+ def valid?(level)
29
+ case level
30
+ when :strong then
31
+ strong?
32
+ when :good then
33
+ good? || strong?
34
+ else
35
+ true
36
+ end
37
+ end
38
+
39
+ # Check if the password has been detected as strong.
40
+ def strong?
41
+ status == :strong
42
+ end
43
+
44
+ # Check if the password has been detected as weak.
45
+ def weak?
46
+ status == :weak
47
+ end
48
+
49
+ # Check if the password has been detected as good.
50
+ def good?
51
+ status == :good
52
+ end
53
+
54
+ # Return the score for the specified rule.
55
+ # Available rules:
56
+ #
57
+ # * :password_size
58
+ # * :numbers
59
+ # * :symbols
60
+ # * :uppercase_lowercase
61
+ # * :numbers_chars
62
+ # * :numbers_symbols
63
+ # * :symbols_chars
64
+ # * :only_chars
65
+ # * :only_numbers
66
+ # * :username
67
+ # * :sequences
68
+ def score_for(name)
69
+ score = 0
70
+
71
+ case name
72
+ when :password_size then
73
+ if password.size < 4
74
+ score = -100
75
+ else
76
+ score = password.size * 4
77
+ end
78
+ when :numbers then
79
+ score = 5 if password =~ MULTIPLE_NUMBERS_RE
80
+ when :symbols then
81
+ score = 5 if password =~ MULTIPLE_SYMBOLS_RE
82
+ when :uppercase_lowercase then
83
+ score = 10 if password =~ UPPERCASE_LOWERCASE_RE
84
+ when :numbers_chars then
85
+ score = 15 if password =~ /[a-z]/i && password =~ /[0-9]/
86
+ when :numbers_symbols then
87
+ score = 15 if password =~ /[0-9]/ && password =~ SYMBOL_RE
88
+ when :symbols_chars then
89
+ score = 15 if password =~ /[a-z]/i && password =~ SYMBOL_RE
90
+ when :only_chars then
91
+ score = -15 if password =~ /^[a-z]+$/i
92
+ when :only_numbers then
93
+ score = -15 if password =~ /^\d+$/
94
+ when :username then
95
+ if password == username
96
+ score = -100
97
+ else
98
+ score = -15 if password =~ /#{Regexp.escape(username)}/
99
+ end
100
+ when :sequences then
101
+ score = -15 * detect_sequences(password)
102
+ score += -15 * detect_sequences(password.to_s.reverse)
103
+ end
104
+
105
+ score
106
+ end
107
+
108
+ def test
109
+ @score = 0
110
+ @score += score_for(:password_size)
111
+ @score += score_for(:numbers)
112
+ @score += score_for(:symbols)
113
+ @score += score_for(:uppercase_lowercase)
114
+ @score += score_for(:numbers_chars)
115
+ @score += score_for(:numbers_symbols)
116
+ @score += score_for(:symbols_chars)
117
+ @score += score_for(:only_chars)
118
+ @score += score_for(:only_numbers)
119
+ @score += score_for(:username)
120
+ @score += score_for(:sequences)
121
+
122
+ @score = 0 if score < 0
123
+ @score = 100 if score > 100
124
+
125
+ @status = :weak if score < 35
126
+ @status = :good if score >= 35 && score < 70
127
+ @status = :strong if score >= 70
128
+
129
+ score
130
+ end
131
+
132
+ def detect_sequences(text) # :nodoc:
133
+ matches = 0
134
+ sequence_size = 0
135
+ bytes = []
136
+
137
+ text.to_s.each_byte do |byte|
138
+ previous_byte = bytes.last
139
+ bytes << byte
140
+
141
+ if previous_byte && ((byte == previous_byte + 1) || (previous_byte == byte))
142
+ sequence_size += 1
143
+ else
144
+ sequence_size = 0
145
+ end
146
+
147
+ matches += 1 if sequence_size == 2
148
+ end
149
+
150
+ matches
151
+ end
152
+ end
153
+ end