nobspw 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +54 -14
- data/lib/active_model/validations/password_validator.rb +22 -21
- data/lib/nobspw.rb +1 -0
- data/lib/nobspw/configuration.rb +2 -0
- data/lib/nobspw/password_checker.rb +3 -85
- data/lib/nobspw/validation_methods.rb +91 -0
- data/lib/nobspw/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48035c919105e867a0aa76612c6dcf13708da443
|
4
|
+
data.tar.gz: ab7cdfe7326b2c6bcf9e4afdf8518a17dd21bec2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 628ec2cae8c6ea70c2a43027e749bf6f1c3c1f1b4b825ed1e0b24a429cea18d043ee25cbef04a37b73dd49f7ee7343cf468e99a50a21e9f05ee458951f43a5ab
|
7
|
+
data.tar.gz: cd3b210d94da9263eb497577d93fb93bc36d8cd45fd9e58ca0267019c646c426832ed9af2ccc001bb2a94a29e05a8aa02ac0b94c96aaf01fb4cf3aebc9bea428
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# NOBSPW - No Bullshit Password strength checker
|
2
2
|
|
3
|
+
[](https://travis-ci.org/cmer/nobspw)
|
4
|
+
|
3
5
|
NOBSPW is simple, no non-sense password strength checker written in Ruby. It does NOT validate against [bullshit password rules](https://twitter.com/codinghorror/status/631238409269309440?ref_src=twsrc%5Etfw) such as:
|
4
6
|
|
5
7
|
- must contain uppercase _(bullshit!)_
|
@@ -73,9 +75,9 @@ validates :password, presence: true, password: true, if: -> { new_record? || cha
|
|
73
75
|
|
74
76
|
PasswordValidator will try to guess the correct field name for each `PasswordChecker` argument as follow:
|
75
77
|
|
76
|
-
- `username`: `username user_name user screenname screen_name`
|
77
|
-
- `name`: `name full_name first_name+last_name`
|
78
|
-
- `email`: `email email_address`
|
78
|
+
- `username`: `username` `user_name` `user` `screenname` `screen_name`
|
79
|
+
- `name`: `name` `full_name` `first_name+last_name`
|
80
|
+
- `email`: `email` `email_address`
|
79
81
|
|
80
82
|
If you have field names different than above, you can tell `PasswordValidator` which fields to use for specific attributes:
|
81
83
|
|
@@ -86,23 +88,61 @@ validates :password, presence: true,
|
|
86
88
|
if: -> { new_record? || changes[:password] }
|
87
89
|
```
|
88
90
|
|
89
|
-
|
91
|
+
## Validations
|
90
92
|
|
91
|
-
NOBSPW currently
|
93
|
+
NOBSPW currently validates for the following, in this order:
|
92
94
|
|
93
95
|
```ruby
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
96
|
+
name_included_in_password?
|
97
|
+
email_included_in_password?
|
98
|
+
domain_included_in_password?
|
99
|
+
password_too_short?
|
100
|
+
password_too_long?
|
101
|
+
not_enough_unique_characters?
|
102
|
+
password_not_allowed?
|
103
|
+
password_too_common?
|
102
104
|
```
|
103
|
-
|
104
105
|
If any of these tests fail, they'll be returned by `#reasons`, or with Rails, they'll be added to `errors[:password]`.
|
105
106
|
|
107
|
+
|
108
|
+
## Custom Validations
|
109
|
+
|
110
|
+
It is possible and easy to add your own validations, or remove default ones.
|
111
|
+
|
112
|
+
### Adding a custom validation
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
module NOBSPW::ValidationMethods
|
116
|
+
def contains_letter_a?
|
117
|
+
# This is obviously a silly validation. Don't do this!
|
118
|
+
# If method returns true, it means that it FAILED validation.
|
119
|
+
# For example, it does contain the letter a and it's not considered acceptable.
|
120
|
+
!!@password.downcase.index('a')
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
NOBSPW.configuration.validation_methods << :contains_letter_a?
|
125
|
+
|
126
|
+
# if using the Rails validator, you also need to define the error message:
|
127
|
+
ActiveModel::Validations::PasswordValidator.error_messages[:contains_letter_a] = \
|
128
|
+
'contains an unacceptable character'
|
129
|
+
|
130
|
+
```
|
131
|
+
|
132
|
+
### Removing a built-in validation
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
NOBSPW.configuration.validation_methods.delete(:domain_included_in_password?)
|
136
|
+
```
|
137
|
+
|
138
|
+
### Localization
|
139
|
+
|
140
|
+
You can localize all error messages, including custom ones, using I18n.
|
141
|
+
|
142
|
+
See `PasswordValidator#get_message` and `PasswordValidator#DEFAULT_ERROR_MESSAGES` for implementation details.
|
143
|
+
|
144
|
+
|
145
|
+
|
106
146
|
## License
|
107
147
|
|
108
148
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -1,4 +1,16 @@
|
|
1
1
|
class ActiveModel::Validations::PasswordValidator < ActiveModel::EachValidator
|
2
|
+
DEFAULT_ERROR_MESSAGES = {
|
3
|
+
name_included_in_password: 'is too similar to your name',
|
4
|
+
email_included_in_password: 'is too similar to your email',
|
5
|
+
domain_included_in_password: 'is too similar to this domain name',
|
6
|
+
password_too_short: 'is too short',
|
7
|
+
password_too_long: 'is too long',
|
8
|
+
not_enough_unique_characters: 'does not have enough unique characters',
|
9
|
+
password_not_allowed: 'is not allowed',
|
10
|
+
password_too_common: 'is too common',
|
11
|
+
fallback: 'is not valid'
|
12
|
+
}
|
13
|
+
|
2
14
|
def validate_each(record, attribute, value)
|
3
15
|
pc = NOBSPW::PasswordChecker.new password: record.send(attribute),
|
4
16
|
email: email_value(record),
|
@@ -11,6 +23,14 @@ class ActiveModel::Validations::PasswordValidator < ActiveModel::EachValidator
|
|
11
23
|
pc.strong?
|
12
24
|
end
|
13
25
|
|
26
|
+
def error_messages
|
27
|
+
@error_messages ||= DEFAULT_ERROR_MESSAGES
|
28
|
+
end
|
29
|
+
|
30
|
+
def error_messages=(em)
|
31
|
+
@error_messages = em
|
32
|
+
end
|
33
|
+
|
14
34
|
private
|
15
35
|
|
16
36
|
def email_value(record)
|
@@ -57,26 +77,7 @@ class ActiveModel::Validations::PasswordValidator < ActiveModel::EachValidator
|
|
57
77
|
end
|
58
78
|
|
59
79
|
def get_message(reason)
|
60
|
-
|
61
|
-
|
62
|
-
I18n.t 'password_validator.is_too_similar_to_your_name', default: 'is too similar to your name'
|
63
|
-
when :email_included_in_password
|
64
|
-
I18n.t 'password_validator.is_too_similar_to_your_email', default: 'is too similar to your email'
|
65
|
-
when :domain_included_in_password
|
66
|
-
I18n.t 'password_validator.is_too_similar_to_this_domain_name', default: 'is too similar to this domain name'
|
67
|
-
when :password_too_short
|
68
|
-
I18n.t 'password_validator.is_too_short', default: 'is too short'
|
69
|
-
when :password_too_long
|
70
|
-
I18n.t 'password_validator.is_too_long', default: 'is too long'
|
71
|
-
when :password_blacklisted
|
72
|
-
I18n.t 'password_validator.is_not_allowed', default: 'is not allowed'
|
73
|
-
when :password_too_common
|
74
|
-
I18n.t 'password_validator.is_too_common', default: 'is too common'
|
75
|
-
when :not_enough_unique_characters
|
76
|
-
I18n.t 'password_validator.does_not_have_enough_unique_chars', default: 'does not have enough unique characters'
|
77
|
-
else
|
78
|
-
I18n.t 'password_validator.is_not_valid', default: 'is not valid'
|
79
|
-
end
|
80
|
+
I18n.t "password_validator.#{reason}", default: error_messages[reason] ||
|
81
|
+
error_messages[:fallback]
|
80
82
|
end
|
81
|
-
|
82
83
|
end
|
data/lib/nobspw.rb
CHANGED
@@ -2,6 +2,7 @@ require "nobspw/version"
|
|
2
2
|
|
3
3
|
module NOBSPW
|
4
4
|
autoload :PasswordChecker, 'nobspw/password_checker'
|
5
|
+
autoload :ValidationMethods, 'nobspw/validation_methods'
|
5
6
|
autoload :Configuration, 'nobspw/configuration'
|
6
7
|
|
7
8
|
if (defined?(::ActiveModel) && ActiveModel::VERSION::MAJOR >= 4) && defined?(I18n)
|
data/lib/nobspw/configuration.rb
CHANGED
@@ -7,6 +7,7 @@ module NOBSPW
|
|
7
7
|
attr_accessor :grep_path
|
8
8
|
attr_accessor :domain_name
|
9
9
|
attr_accessor :blacklist
|
10
|
+
attr_accessor :validation_methods
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@min_password_length = 10
|
@@ -16,6 +17,7 @@ module NOBSPW
|
|
16
17
|
@grep_path = `which grep`.strip
|
17
18
|
@domain_name = nil
|
18
19
|
@blacklist = nil
|
20
|
+
@validation_methods = NOBSPW::ValidationMethods::DEFAULT_VALIDATION_METHODS
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
@@ -1,13 +1,6 @@
|
|
1
1
|
module NOBSPW
|
2
2
|
class PasswordChecker
|
3
|
-
|
4
|
-
email_included_in_password
|
5
|
-
domain_included_in_password
|
6
|
-
password_too_short
|
7
|
-
password_too_long
|
8
|
-
not_enough_unique_characters
|
9
|
-
password_blacklisted
|
10
|
-
password_too_common)
|
3
|
+
include NOBSPW::ValidationMethods
|
11
4
|
|
12
5
|
def initialize(name: nil, username: nil, email: nil, password:)
|
13
6
|
@name, @username, @email, @password = \
|
@@ -36,90 +29,15 @@ module NOBSPW
|
|
36
29
|
def check_password
|
37
30
|
@weak_password_reasons = []
|
38
31
|
|
39
|
-
|
40
|
-
@weak_password_reasons <<
|
32
|
+
NOBSPW.configuration.validation_methods.each do |method|
|
33
|
+
@weak_password_reasons << method.to_s.sub(/\?$/, '').to_sym if send("#{method}")
|
41
34
|
end
|
42
35
|
|
43
36
|
@strong = @weak_password_reasons.empty?
|
44
37
|
end
|
45
38
|
|
46
|
-
def name_included_in_password?
|
47
|
-
return nil unless @name
|
48
|
-
words = remove_word_separators(@name).split(' ')
|
49
|
-
words_included_in_password?(words)
|
50
|
-
end
|
51
|
-
|
52
|
-
def email_included_in_password?
|
53
|
-
return nil unless @email
|
54
|
-
words = remove_word_separators(email_without_extension(@email)).split(' ')
|
55
|
-
words_included_in_password?(words)
|
56
|
-
end
|
57
|
-
|
58
|
-
def domain_included_in_password?
|
59
|
-
domain = NOBSPW.configuration.domain_name
|
60
|
-
return nil unless domain
|
61
|
-
domain = strip_extension_from_domain(domain)
|
62
|
-
domain_without_separator = remove_word_separators(domain).gsub(' ', '')
|
63
|
-
words_included_in_password?([domain, domain_without_separator])
|
64
|
-
end
|
65
|
-
|
66
|
-
def password_blacklisted?
|
67
|
-
return nil unless NOBSPW.configuration.blacklist
|
68
|
-
NOBSPW.configuration.blacklist.include?(@password)
|
69
|
-
end
|
70
|
-
|
71
|
-
def password_too_short?
|
72
|
-
@password.length < NOBSPW.configuration.min_password_length
|
73
|
-
end
|
74
|
-
|
75
|
-
def password_too_long?
|
76
|
-
@password.length > NOBSPW.configuration.max_password_length
|
77
|
-
end
|
78
|
-
|
79
|
-
def not_enough_unique_characters?
|
80
|
-
unique = @password.split(//).uniq.size
|
81
|
-
minimum = NOBSPW.configuration.min_unique_characters
|
82
|
-
!(unique >= minimum)
|
83
|
-
end
|
84
|
-
|
85
|
-
def words_included_in_password?(words)
|
86
|
-
downcased_pw = @password.downcase
|
87
|
-
words.each do |word|
|
88
|
-
return true if word.length > 2 && downcased_pw.index(word.downcase)
|
89
|
-
end; false
|
90
|
-
end
|
91
|
-
|
92
|
-
def password_too_common?
|
93
|
-
`#{grep_command(NOBSPW.configuration.dictionary_path)}`
|
94
|
-
|
95
|
-
case $?.exitstatus
|
96
|
-
when 0
|
97
|
-
true
|
98
|
-
when 1
|
99
|
-
false
|
100
|
-
when 127
|
101
|
-
raise StandardError.new("Grep not found at: #{NOBSPW.configuration.grep_path}")
|
102
|
-
else
|
103
|
-
false
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
39
|
def grep_command(path)
|
108
40
|
"#{NOBSPW.configuration.grep_path} '^#{@password}$' #{path}"
|
109
41
|
end
|
110
|
-
|
111
|
-
def email_without_extension(email)
|
112
|
-
name, domain, whatev = email.split("@", 3)
|
113
|
-
"#{name}@#{strip_extension_from_domain(domain)}"
|
114
|
-
end
|
115
|
-
|
116
|
-
def strip_extension_from_domain(domain)
|
117
|
-
domain.split(".").first
|
118
|
-
end
|
119
|
-
|
120
|
-
def remove_word_separators(str)
|
121
|
-
str.gsub(/-|_|\.|\'|\"|\@/, ' ')
|
122
|
-
end
|
123
42
|
end
|
124
|
-
|
125
43
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module NOBSPW
|
2
|
+
module ValidationMethods
|
3
|
+
DEFAULT_VALIDATION_METHODS = %i(name_included_in_password?
|
4
|
+
email_included_in_password?
|
5
|
+
domain_included_in_password?
|
6
|
+
password_too_short?
|
7
|
+
password_too_long?
|
8
|
+
not_enough_unique_characters?
|
9
|
+
password_not_allowed?
|
10
|
+
password_too_common?)
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def name_included_in_password?
|
15
|
+
return nil unless @name
|
16
|
+
words = remove_word_separators(@name).split(' ')
|
17
|
+
words_included_in_password?(words)
|
18
|
+
end
|
19
|
+
|
20
|
+
def email_included_in_password?
|
21
|
+
return nil unless @email
|
22
|
+
words = remove_word_separators(email_without_extension(@email)).split(' ')
|
23
|
+
words_included_in_password?(words)
|
24
|
+
end
|
25
|
+
|
26
|
+
def domain_included_in_password?
|
27
|
+
domain = NOBSPW.configuration.domain_name
|
28
|
+
return nil unless domain
|
29
|
+
domain = strip_extension_from_domain(domain)
|
30
|
+
domain_without_separator = remove_word_separators(domain).gsub(' ', '')
|
31
|
+
words_included_in_password?([domain, domain_without_separator])
|
32
|
+
end
|
33
|
+
|
34
|
+
def password_not_allowed?
|
35
|
+
return nil unless NOBSPW.configuration.blacklist
|
36
|
+
NOBSPW.configuration.blacklist.include?(@password)
|
37
|
+
end
|
38
|
+
|
39
|
+
def password_too_short?
|
40
|
+
@password.length < NOBSPW.configuration.min_password_length
|
41
|
+
end
|
42
|
+
|
43
|
+
def password_too_long?
|
44
|
+
@password.length > NOBSPW.configuration.max_password_length
|
45
|
+
end
|
46
|
+
|
47
|
+
def not_enough_unique_characters?
|
48
|
+
unique = @password.split(//).uniq.size
|
49
|
+
minimum = NOBSPW.configuration.min_unique_characters
|
50
|
+
!(unique >= minimum)
|
51
|
+
end
|
52
|
+
|
53
|
+
def words_included_in_password?(words)
|
54
|
+
downcased_pw = @password.downcase
|
55
|
+
words.each do |word|
|
56
|
+
return true if word.length > 2 && downcased_pw.index(word.downcase)
|
57
|
+
end; false
|
58
|
+
end
|
59
|
+
|
60
|
+
def password_too_common?
|
61
|
+
`#{grep_command(NOBSPW.configuration.dictionary_path)}`
|
62
|
+
|
63
|
+
case $?.exitstatus
|
64
|
+
when 0
|
65
|
+
true
|
66
|
+
when 1
|
67
|
+
false
|
68
|
+
when 127
|
69
|
+
raise StandardError.new("Grep not found at: #{NOBSPW.configuration.grep_path}")
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Helper methods
|
76
|
+
|
77
|
+
def email_without_extension(email)
|
78
|
+
name, domain, whatev = email.split("@", 3)
|
79
|
+
"#{name}@#{strip_extension_from_domain(domain)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def strip_extension_from_domain(domain)
|
83
|
+
domain.split(".").first
|
84
|
+
end
|
85
|
+
|
86
|
+
def remove_word_separators(str)
|
87
|
+
str.gsub(/-|_|\.|\'|\"|\@/, ' ')
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
data/lib/nobspw/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nobspw
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carl Mercier
|
@@ -173,6 +173,7 @@ files:
|
|
173
173
|
- lib/nobspw.rb
|
174
174
|
- lib/nobspw/configuration.rb
|
175
175
|
- lib/nobspw/password_checker.rb
|
176
|
+
- lib/nobspw/validation_methods.rb
|
176
177
|
- lib/nobspw/version.rb
|
177
178
|
- nobspw.gemspec
|
178
179
|
homepage: https://github.com/cmer/nobspw
|