strong_password 0.0.4 → 0.0.10
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 +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +17 -0
- data/CHANGELOG +16 -1
- data/Gemfile +7 -2
- data/README.md +38 -24
- data/Rakefile +5 -0
- data/lib/active_model/validations/password_strength_validator.rb +4 -4
- data/lib/strong_password.rb +2 -2
- data/lib/strong_password/dictionary_adjuster.rb +16 -12
- data/lib/strong_password/entropy_calculator.rb +8 -8
- data/lib/strong_password/locale/en.yml +1 -1
- data/lib/strong_password/nist_bonus_bits.rb +5 -5
- data/lib/strong_password/password_variants.rb +18 -55
- data/lib/strong_password/qwerty_adjuster.rb +58 -56
- data/lib/strong_password/railtie.rb +1 -1
- data/lib/strong_password/strength_checker.rb +41 -20
- data/lib/strong_password/version.rb +1 -1
- data/spec/spec_helper.rb +8 -2
- data/spec/strong_password/dictionary_adjuster_spec.rb +18 -18
- data/spec/strong_password/entropy_calculator_spec.rb +4 -4
- data/spec/strong_password/nist_bonus_bits_spec.rb +5 -5
- data/spec/strong_password/password_variants_spec.rb +14 -14
- data/spec/strong_password/qwerty_adjuster_spec.rb +26 -16
- data/spec/strong_password/strength_checker_spec.rb +31 -8
- data/spec/validation/strength_validator_spec.rb +12 -8
- data/strong_password.gemspec +3 -2
- metadata +29 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4559a04254bce83f7f49ed741cacc81df19a55ca3b7512ffb8fbd63da5ec0a57
|
4
|
+
data.tar.gz: 2164536b2690a5946796f874b00db0a80228bb72c19f3d3437625a332a792fa9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75dbfd339fcf1c8a4082ff02bf5f8326a0299fcb600a93e2126e10a409abd3008002c5f705537710f40e9c9989d29fc3feb8ffa22b70c121fb81c60f363f9f23
|
7
|
+
data.tar.gz: ea1905e903579dc290b544e99476bbc88585cdff81c45c6e492969978bc465e4ce2e59e160a64c96c05fa16ebd98efc995d60bc196f3e30b5bcb53e289008d72
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.2.3
|
4
|
+
- 2.1.7
|
5
|
+
- ruby-head
|
6
|
+
env:
|
7
|
+
- "RAILS_VERSION=4.2"
|
8
|
+
- "RAILS_VERSION=4.1"
|
9
|
+
- "RAILS_VERSION=4.0"
|
10
|
+
- "RAILS_VERSION=3.2"
|
11
|
+
- "RAILS_VERSION=3.1"
|
12
|
+
- "RAILS_VERSION=3.0"
|
13
|
+
- "RAILS_VERSION=master"
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: ruby-head
|
17
|
+
- env: "RAILS_VERSION=master"
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## v0.0.10
|
2
|
+
* jfabre - Remove ruby 2.7 warning
|
3
|
+
## v0.0.8
|
4
|
+
This release breaks backwards compatibility if you are using the lib components
|
5
|
+
directly. If you are using the ActiveModel validations in a Rails project then
|
6
|
+
you should be unaffected.
|
7
|
+
|
8
|
+
The major changes when used directly are that you now initialize the objects
|
9
|
+
with the various strength options instead of the password and then provide the
|
10
|
+
password to the various methods. The README section on standalone usage has
|
11
|
+
been updated to reflect this change.
|
12
|
+
|
13
|
+
* jacobat - Performance improvements
|
14
|
+
* jacksenechal - Updated README
|
15
|
+
|
1
16
|
## v0.0.4
|
2
17
|
* peterkovacs - Increase size of password dictionary
|
3
18
|
* peterkovacs - Swapped in a cleaner, more efficient comparison algorithm
|
@@ -7,4 +22,4 @@
|
|
7
22
|
|
8
23
|
## v0.0.1
|
9
24
|
|
10
|
-
* Initial release
|
25
|
+
* Initial release
|
data/Gemfile
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
2
3
|
|
3
4
|
gemspec
|
4
5
|
|
5
|
-
version = ENV["RAILS_VERSION"] || "
|
6
|
+
version = ENV["RAILS_VERSION"] || "4.2"
|
6
7
|
|
7
8
|
rails = case version
|
8
9
|
when "master"
|
@@ -11,4 +12,8 @@ else
|
|
11
12
|
"~> #{version}.0"
|
12
13
|
end
|
13
14
|
|
14
|
-
gem "rails", rails
|
15
|
+
gem "rails", rails
|
16
|
+
group :test do
|
17
|
+
gem "simplecov", require: false
|
18
|
+
gem "simplecov-console", require: false
|
19
|
+
end
|
data/README.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
[](https://travis-ci.org/bdmac/strong_password)
|
2
|
+
[](https://codeclimate.com/github/bdmac/strong_password)
|
3
|
+
[](https://codeclimate.com/github/bdmac/strong_password/coverage)
|
4
|
+
|
1
5
|
# StrongPassword
|
2
6
|
|
3
7
|
StrongPassword provides entropy-based password strength checking for your apps.
|
@@ -15,7 +19,9 @@ NOTE: StrongPassword requires the use of Ruby 2.0. Upgrade if you haven't alrea
|
|
15
19
|
|
16
20
|
Add this line to your application's Gemfile:
|
17
21
|
|
18
|
-
|
22
|
+
```ruby
|
23
|
+
gem 'strong_password', '~> 0.0.9'
|
24
|
+
```
|
19
25
|
|
20
26
|
And then execute:
|
21
27
|
|
@@ -61,15 +67,15 @@ class User
|
|
61
67
|
end
|
62
68
|
```
|
63
69
|
|
64
|
-
The default validation message is "
|
65
|
-
|
70
|
+
The default validation message is "is too weak". Rails will display a field of `:password` having too weak of a password with `full_error_messages` as "Password is too weak". If you would like to customize the error message, you can override it in your locale file by setting a value for this key:
|
71
|
+
|
66
72
|
|
67
73
|
```yml
|
68
74
|
en:
|
69
75
|
errors:
|
70
76
|
messages:
|
71
77
|
password:
|
72
|
-
password_strength: "
|
78
|
+
password_strength: "is a terrible password, try again!"
|
73
79
|
```
|
74
80
|
|
75
81
|
### Standalone
|
@@ -77,19 +83,23 @@ en:
|
|
77
83
|
StrongPassword can also be used standalone if you need to. There are a few helper methods for determining whether a
|
78
84
|
password is strong or not. You can also directly access the entropy calculations if you want.
|
79
85
|
|
80
|
-
```
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
86
|
+
```console
|
87
|
+
irb(main):004:0> checker = StrongPassword::StrengthChecker.new
|
88
|
+
=> #<StrongPassword::StrengthChecker:0x00007f985509db30 @min_entropy=18, @use_dictionary=false, @min_word_length=4, @extra_dictionary_words=[]>
|
89
|
+
irb(main):005:0> checker.is_strong?("password")
|
90
|
+
=> false
|
91
|
+
irb(main):006:0> checker.is_weak?("password")
|
92
|
+
=> true
|
93
|
+
irb(main):007:0> checker = StrongPassword::StrengthChecker.new(min_entropy: 2)
|
94
|
+
=> #<StrongPassword::StrengthChecker:0x00007f9855147bd0 @min_entropy=2, @use_dictionary=false, @min_word_length=4, @extra_dictionary_words=[]>
|
95
|
+
irb(main):008:0> checker.calculate_entropy("password")
|
96
|
+
=> 15.5
|
97
|
+
irb(main):009:0> checker.is_strong?("password")
|
98
|
+
=> true
|
99
|
+
irb(main):010:0> checker = StrongPassword::StrengthChecker.new(use_dictionary: true)
|
100
|
+
=> #<StrongPassword::StrengthChecker:0x00007f98550ee008 @min_entropy=18, @use_dictionary=true, @min_word_length=4, @extra_dictionary_words=[]>
|
101
|
+
irb(main):011:0> checker.calculate_entropy("password")
|
102
|
+
=> 2
|
93
103
|
```
|
94
104
|
|
95
105
|
## Details
|
@@ -141,17 +151,21 @@ disallowed by the strength checker.
|
|
141
151
|
|
142
152
|
## Todo
|
143
153
|
|
144
|
-
1.
|
145
|
-
the 500 most common passwords. That also means it's not a true "dictionary" check...
|
146
|
-
2. Add a common password adjuster that basically works like the existing DictionaryAdjuster but does
|
154
|
+
1. Add a common password adjuster that basically works like the existing DictionaryAdjuster but does
|
147
155
|
not stop at the first found word. Stopping at the first word make sense if you have a 300,000 word
|
148
|
-
dictionary of the English language but not so much when you're only talking about the
|
156
|
+
dictionary of the English language but not so much when you're only talking about the 10,000 most
|
149
157
|
common passwords.
|
150
158
|
|
159
|
+
## Running the tests
|
160
|
+
|
161
|
+
To run the tests, install the gems with `bundle install`. Then run `rake`.
|
162
|
+
|
151
163
|
## Contributing
|
152
164
|
|
153
165
|
1. Fork it
|
154
166
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
155
|
-
3.
|
156
|
-
4.
|
157
|
-
5.
|
167
|
+
3. Make your changes
|
168
|
+
4. Test the changes and make sure existing tests pass
|
169
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
170
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
171
|
+
7. Create new Pull Request
|
data/Rakefile
CHANGED
@@ -4,9 +4,9 @@ module ActiveModel
|
|
4
4
|
module Validations
|
5
5
|
class PasswordStrengthValidator < ActiveModel::EachValidator
|
6
6
|
def validate_each(object, attribute, value)
|
7
|
-
ps = ::StrongPassword::StrengthChecker.new(
|
8
|
-
unless ps.is_strong?(
|
9
|
-
object.errors.add(attribute, :'password.password_strength', options.merge(:value => value.to_s))
|
7
|
+
ps = ::StrongPassword::StrengthChecker.new(**strength_options(options, object))
|
8
|
+
unless ps.is_strong?(value.to_s)
|
9
|
+
object.errors.add(attribute, :'password.password_strength', **(options.merge(:value => value.to_s)))
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
@@ -40,4 +40,4 @@ module ActiveModel
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
|
-
end
|
43
|
+
end
|
data/lib/strong_password.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'active_model/validations'
|
1
|
+
require 'active_model/validations' if defined?(ActiveModel)
|
2
2
|
|
3
3
|
require 'strong_password/version'
|
4
4
|
require 'strong_password/nist_bonus_bits'
|
@@ -12,4 +12,4 @@ require 'active_model/validations/password_strength_validator' if defined?(Activ
|
|
12
12
|
module StrongPassword
|
13
13
|
end
|
14
14
|
|
15
|
-
I18n.load_path << File.dirname(__FILE__) + '/strong_password/locale/en.yml' if defined?(I18n)
|
15
|
+
I18n.load_path << File.dirname(__FILE__) + '/strong_password/locale/en.yml' if defined?(I18n)
|
@@ -967,20 +967,20 @@ module StrongPassword
|
|
967
967
|
hounds honeydew hooters1 hoes howie hevnm4 hugohugo eighty epson evangeli
|
968
968
|
eeeee1 eyphed ).sort_by(&:length).reverse
|
969
969
|
|
970
|
-
attr_reader :
|
970
|
+
attr_reader :min_entropy, :min_word_length, :extra_dictionary_words
|
971
971
|
|
972
|
-
def initialize(
|
973
|
-
@
|
972
|
+
def initialize(min_entropy: 18, min_word_length: 4, extra_dictionary_words: [])
|
973
|
+
@min_entropy = min_entropy
|
974
|
+
@min_word_length = min_word_length
|
975
|
+
@extra_dictionary_words = extra_dictionary_words
|
974
976
|
end
|
975
977
|
|
976
|
-
def is_strong?(
|
977
|
-
adjusted_entropy(
|
978
|
-
min_word_length: min_word_length,
|
979
|
-
extra_dictionary_words: extra_dictionary_words) >= min_entropy
|
978
|
+
def is_strong?(password)
|
979
|
+
adjusted_entropy(password) >= min_entropy
|
980
980
|
end
|
981
981
|
|
982
|
-
def is_weak?(
|
983
|
-
!is_strong?(
|
982
|
+
def is_weak?(password)
|
983
|
+
!is_strong?(password)
|
984
984
|
end
|
985
985
|
|
986
986
|
# Returns the minimum entropy for the passwords dictionary adjustments.
|
@@ -988,13 +988,17 @@ module StrongPassword
|
|
988
988
|
# processing.
|
989
989
|
# Note that we only check for the first matching word up to the threshhold if set.
|
990
990
|
# Subsequent matching words are not deductd.
|
991
|
-
def adjusted_entropy(
|
992
|
-
|
991
|
+
def adjusted_entropy(password)
|
992
|
+
base_password = password.downcase
|
993
993
|
min_entropy = EntropyCalculator.calculate(base_password)
|
994
994
|
# Process the passwords, while looking for possible matching words in the dictionary.
|
995
995
|
PasswordVariants.all_variants(base_password).inject( min_entropy ) do |min_entropy, variant|
|
996
996
|
[ min_entropy, EntropyCalculator.calculate( variant.sub( dictionary_words, '*' ) ) ].min
|
997
997
|
end
|
998
998
|
end
|
999
|
+
|
1000
|
+
def dictionary_words
|
1001
|
+
@dictionary_words ||= Regexp.union( ( extra_dictionary_words + COMMON_PASSWORDS ).compact.reject{ |i| i.length < min_word_length } )
|
1002
|
+
end
|
999
1003
|
end
|
1000
|
-
end
|
1004
|
+
end
|
@@ -8,7 +8,7 @@ module StrongPassword
|
|
8
8
|
bits(password)
|
9
9
|
end
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
# The basic NIST entropy calculation is based solely
|
13
13
|
# on the length of the password in question.
|
14
14
|
def self.bits(password)
|
@@ -24,7 +24,7 @@ module StrongPassword
|
|
24
24
|
end
|
25
25
|
bits + NistBonusBits.bonus_bits(password)
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
# A modified version of the basic entropy calculation
|
29
29
|
# which lowers the amount of entropy gained for each
|
30
30
|
# repeated character in the password
|
@@ -36,9 +36,9 @@ module StrongPassword
|
|
36
36
|
end
|
37
37
|
bits + NistBonusBits.bonus_bits(password)
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
private
|
41
|
-
|
41
|
+
|
42
42
|
def self.bit_value_at_position(position, base = 1)
|
43
43
|
if position > 19
|
44
44
|
return base
|
@@ -50,17 +50,17 @@ module StrongPassword
|
|
50
50
|
return 4
|
51
51
|
end
|
52
52
|
end
|
53
|
-
|
53
|
+
|
54
54
|
class EntropyResolver
|
55
55
|
BASE_VALUE = 1
|
56
56
|
REPEAT_WEAKENING_FACTOR = 0.75
|
57
|
-
|
57
|
+
|
58
58
|
attr_reader :char_multiplier
|
59
|
-
|
59
|
+
|
60
60
|
def initialize
|
61
61
|
@char_multiplier = {}
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
# Returns the current entropy value for a character and weakens the entropy
|
65
65
|
# for future calls for the same character.
|
66
66
|
def entropy_for(char)
|
@@ -1,26 +1,26 @@
|
|
1
1
|
module StrongPassword
|
2
2
|
module NistBonusBits
|
3
3
|
@@bonus_bits_for_password = {}
|
4
|
-
|
4
|
+
|
5
5
|
# NIST password strength rules allow up to 6 bonus bits for mixed case and non-alphabetic
|
6
6
|
def self.bonus_bits(password)
|
7
7
|
@@bonus_bits_for_password[password] ||= begin
|
8
8
|
calculate_bonus_bits_for(password)
|
9
9
|
end
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
# This smells bad as it's only used for testing...
|
13
13
|
def self.reset_bonus_cache!
|
14
14
|
@@bonus_bits_for_password = {}
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def self.calculate_bonus_bits_for(password)
|
18
18
|
upper = !!(password =~ /[[:upper:]]/)
|
19
19
|
lower = !!(password =~ /[[:lower:]]/)
|
20
20
|
numeric = !!(password =~ /[[:digit:]]/)
|
21
21
|
other = !!(password =~ /[^a-zA-Z0-9 ]/)
|
22
22
|
space = !!(password =~ / /)
|
23
|
-
|
23
|
+
|
24
24
|
# I had this condensed to nested ternaries but that shit was ugly
|
25
25
|
bonus_bits = if upper && lower && other && numeric
|
26
26
|
6
|
@@ -42,4 +42,4 @@ module StrongPassword
|
|
42
42
|
bonus_bits
|
43
43
|
end
|
44
44
|
end
|
45
|
-
end
|
45
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module StrongPassword
|
2
2
|
module PasswordVariants
|
3
|
-
|
3
|
+
LEET_SPEAK_REGEXP = /[\@\!\$1234567890]/
|
4
|
+
|
5
|
+
LEET_SPEAK = {
|
4
6
|
"@" => "a",
|
5
7
|
"!" => "i",
|
6
8
|
"$" => "s",
|
@@ -16,37 +18,9 @@ module StrongPassword
|
|
16
18
|
"0" => "o"
|
17
19
|
}
|
18
20
|
|
19
|
-
|
20
|
-
"@" => "a",
|
21
|
-
"!" => "i",
|
22
|
-
"$" => "s",
|
23
|
-
"1" => "l",
|
24
|
-
"2" => "z",
|
25
|
-
"3" => "e",
|
26
|
-
"4" => "a",
|
27
|
-
"5" => "s",
|
28
|
-
"6" => "g",
|
29
|
-
"7" => "t",
|
30
|
-
"8" => "b",
|
31
|
-
"9" => "g",
|
32
|
-
"0" => "o"
|
33
|
-
}
|
21
|
+
LEET_SPEAK_ALT = LEET_SPEAK.dup.merge!("1" => "l")
|
34
22
|
|
35
|
-
|
36
|
-
"z" => "",
|
37
|
-
"x" => "",
|
38
|
-
"c" => "",
|
39
|
-
"v" => "",
|
40
|
-
"b" => "",
|
41
|
-
"n" => "",
|
42
|
-
"m" => "",
|
43
|
-
"," => "",
|
44
|
-
"." => "",
|
45
|
-
"/" => "",
|
46
|
-
"<" => "",
|
47
|
-
">" => "",
|
48
|
-
"?" => ""
|
49
|
-
}
|
23
|
+
BOTTOM_ROW_REGEXP = /[zxcvbnm,\.\/\<\>\?]/
|
50
24
|
|
51
25
|
KEYBOARDMAP_DOWNRIGHT = {
|
52
26
|
"a" => "z",
|
@@ -105,7 +79,7 @@ module StrongPassword
|
|
105
79
|
"p" => "l",
|
106
80
|
"-" => "p"
|
107
81
|
}
|
108
|
-
|
82
|
+
|
109
83
|
# Returns all variants of a given password including the password itself
|
110
84
|
def self.all_variants(password)
|
111
85
|
passwords = [password.downcase]
|
@@ -117,37 +91,26 @@ module StrongPassword
|
|
117
91
|
# Returns all keyboard shifted variants of a given password
|
118
92
|
def self.keyboard_shift_variants(password)
|
119
93
|
password = password.downcase
|
120
|
-
variants = []
|
121
|
-
|
122
|
-
if (password == password.tr(KEYBOARDMAP_DOWN_NOSHIFT.keys.join, KEYBOARDMAP_DOWN_NOSHIFT.values.join))
|
123
|
-
variant = password.tr(KEYBOARDMAP_DOWNRIGHT.keys.join, KEYBOARDMAP_DOWNRIGHT.values.join)
|
124
|
-
variants << variant
|
125
|
-
variants << variant.reverse
|
126
94
|
|
127
|
-
|
128
|
-
|
129
|
-
variants << variant.reverse
|
130
|
-
end
|
131
|
-
variants
|
95
|
+
return [] if password.match(BOTTOM_ROW_REGEXP)
|
96
|
+
variants(password, KEYBOARDMAP_DOWNRIGHT, KEYBOARDMAP_DOWNLEFT)
|
132
97
|
end
|
133
98
|
|
134
99
|
# Returns all leet speak variants of a given password
|
135
100
|
def self.leet_speak_variants(password)
|
136
101
|
password = password.downcase
|
137
|
-
variants = []
|
138
102
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
103
|
+
return [] if !password.match(LEET_SPEAK_REGEXP)
|
104
|
+
variants(password, LEET_SPEAK, LEET_SPEAK_ALT)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
144
108
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
109
|
+
def self.variants(password, *mappings)
|
110
|
+
mappings.flat_map do |map|
|
111
|
+
variant = password.tr(map.keys.join, map.values.join)
|
112
|
+
[variant, variant.reverse]
|
149
113
|
end
|
150
|
-
variants
|
151
114
|
end
|
152
115
|
end
|
153
|
-
end
|
116
|
+
end
|