strong_password 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/CHANGELOG +3 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +1 -0
- data/lib/active_model/validations/password_strength_validator.rb +43 -0
- data/lib/strong_password/dictionary_adjuster.rb +117 -0
- data/lib/strong_password/entropy_calculator.rb +76 -0
- data/lib/strong_password/locale/en.yml +5 -0
- data/lib/strong_password/nist_bonus_bits.rb +45 -0
- data/lib/strong_password/password_variants.rb +153 -0
- data/lib/strong_password/qwerty_adjuster.rb +73 -0
- data/lib/strong_password/railtie.rb +1 -0
- data/lib/strong_password/strength_checker.rb +37 -0
- data/lib/strong_password/version.rb +3 -0
- data/lib/strong_password.rb +15 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/strong_password/dictionary_adjuster_spec.rb +66 -0
- data/spec/strong_password/entropy_calculator_spec.rb +65 -0
- data/spec/strong_password/nist_bonus_bits_spec.rb +45 -0
- data/spec/strong_password/password_variants_spec.rb +67 -0
- data/spec/strong_password/qwerty_adjuster_spec.rb +45 -0
- data/spec/strong_password/strength_checker_spec.rb +64 -0
- data/spec/validation/strength_validator_spec.rb +150 -0
- data/strong_password.gemspec +24 -0
- metadata +121 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
module StrongPassword
|
2
|
+
module PasswordVariants
|
3
|
+
LEET_SPEAK_1 = {
|
4
|
+
"@" => "a",
|
5
|
+
"!" => "i",
|
6
|
+
"$" => "s",
|
7
|
+
"1" => "i",
|
8
|
+
"2" => "z",
|
9
|
+
"3" => "e",
|
10
|
+
"4" => "a",
|
11
|
+
"5" => "s",
|
12
|
+
"6" => "g",
|
13
|
+
"7" => "t",
|
14
|
+
"8" => "b",
|
15
|
+
"9" => "g",
|
16
|
+
"0" => "o"
|
17
|
+
}
|
18
|
+
|
19
|
+
LEET_SPEAK_2 = {
|
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
|
+
}
|
34
|
+
|
35
|
+
KEYBOARDMAP_DOWN_NOSHIFT = {
|
36
|
+
"z" => "",
|
37
|
+
"x" => "",
|
38
|
+
"c" => "",
|
39
|
+
"v" => "",
|
40
|
+
"b" => "",
|
41
|
+
"n" => "",
|
42
|
+
"m" => "",
|
43
|
+
"," => "",
|
44
|
+
"." => "",
|
45
|
+
"/" => "",
|
46
|
+
"<" => "",
|
47
|
+
">" => "",
|
48
|
+
"?" => ""
|
49
|
+
}
|
50
|
+
|
51
|
+
KEYBOARDMAP_DOWNRIGHT = {
|
52
|
+
"a" => "z",
|
53
|
+
"q" => "a",
|
54
|
+
"1" => "q",
|
55
|
+
"s" => "x",
|
56
|
+
"w" => "s",
|
57
|
+
"2" => "w",
|
58
|
+
"d" => "c",
|
59
|
+
"e" => "d",
|
60
|
+
"3" => "e",
|
61
|
+
"f" => "v",
|
62
|
+
"r" => "f",
|
63
|
+
"4" => "r",
|
64
|
+
"g" => "b",
|
65
|
+
"t" => "g",
|
66
|
+
"5" => "t",
|
67
|
+
"h" => "n",
|
68
|
+
"y" => "h",
|
69
|
+
"6" => "y",
|
70
|
+
"j" => "m",
|
71
|
+
"u" => "j",
|
72
|
+
"7" => "u",
|
73
|
+
"i" => "k",
|
74
|
+
"8" => "i",
|
75
|
+
"o" => "l",
|
76
|
+
"9" => "o",
|
77
|
+
"0" => "p"
|
78
|
+
}
|
79
|
+
|
80
|
+
KEYBOARDMAP_DOWNLEFT = {
|
81
|
+
"2" => "q",
|
82
|
+
"w" => "a",
|
83
|
+
"3" => "w",
|
84
|
+
"s" => "z",
|
85
|
+
"e" => "s",
|
86
|
+
"4" => "e",
|
87
|
+
"d" => "x",
|
88
|
+
"r" => "d",
|
89
|
+
"5" => "r",
|
90
|
+
"f" => "c",
|
91
|
+
"t" => "f",
|
92
|
+
"6" => "t",
|
93
|
+
"g" => "v",
|
94
|
+
"y" => "g",
|
95
|
+
"7" => "y",
|
96
|
+
"h" => "b",
|
97
|
+
"u" => "h",
|
98
|
+
"8" => "u",
|
99
|
+
"j" => "n",
|
100
|
+
"i" => "j",
|
101
|
+
"9" => "i",
|
102
|
+
"k" => "m",
|
103
|
+
"o" => "k",
|
104
|
+
"0" => "o",
|
105
|
+
"p" => "l",
|
106
|
+
"-" => "p"
|
107
|
+
}
|
108
|
+
|
109
|
+
# Returns all variants of a given password including the password itself
|
110
|
+
def self.all_variants(password)
|
111
|
+
passwords = [password.dup.downcase]
|
112
|
+
passwords += keyboard_shift_variants(password)
|
113
|
+
passwords += leet_speak_variants(password)
|
114
|
+
passwords.uniq
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns all keyboard shifted variants of a given password
|
118
|
+
def self.keyboard_shift_variants(password)
|
119
|
+
password = password.dup.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
|
+
|
127
|
+
variant = password.tr(KEYBOARDMAP_DOWNLEFT.keys.join, KEYBOARDMAP_DOWNLEFT.values.join)
|
128
|
+
variants << variant
|
129
|
+
variants << variant.reverse
|
130
|
+
end
|
131
|
+
variants
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns all leet speak variants of a given password
|
135
|
+
def self.leet_speak_variants(password)
|
136
|
+
password = password.dup.downcase
|
137
|
+
variants = []
|
138
|
+
|
139
|
+
leet = password.tr(LEET_SPEAK_1.keys.join, LEET_SPEAK_1.values.join)
|
140
|
+
if leet != password
|
141
|
+
variants << leet
|
142
|
+
variants << leet.reverse
|
143
|
+
end
|
144
|
+
|
145
|
+
leet_l = password.tr(LEET_SPEAK_2.keys.join, LEET_SPEAK_2.values.join)
|
146
|
+
if (leet_l != password && leet_l != leet)
|
147
|
+
variants << leet_l
|
148
|
+
variants << leet_l.reverse
|
149
|
+
end
|
150
|
+
variants
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module StrongPassword
|
2
|
+
class QwertyAdjuster
|
3
|
+
QWERTY_STRINGS = [
|
4
|
+
"1234567890-",
|
5
|
+
"qwertyuiop",
|
6
|
+
"asdfghjkl;",
|
7
|
+
"zxcvbnm,./",
|
8
|
+
"1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]:?_{\"+}",
|
9
|
+
"1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p",
|
10
|
+
"qazwsxedcrfvtgbyhnujmik,ol.p;/-['=]:?_{\"+}",
|
11
|
+
"qazwsxedcrfvtgbyhnujmikolp",
|
12
|
+
"]\"/=[;.-pl,0okm9ijn8uhb7ygv6tfc5rdx4esz3wa2q1",
|
13
|
+
"pl0okm9ijn8uhb7ygv6tfc5rdx4esz3wa2q1",
|
14
|
+
"]\"/[;.pl,okmijnuhbygvtfcrdxeszwaq",
|
15
|
+
"plokmijnuhbygvtfcrdxeszwaq",
|
16
|
+
"014725836914702583697894561230258/369*+-*/",
|
17
|
+
"abcdefghijklmnopqrstuvwxyz"
|
18
|
+
]
|
19
|
+
|
20
|
+
attr_reader :base_password
|
21
|
+
|
22
|
+
def initialize(password)
|
23
|
+
@base_password = password.dup.downcase
|
24
|
+
end
|
25
|
+
|
26
|
+
def is_strong?(min_entropy: 18)
|
27
|
+
adjusted_entropy(entropy_threshhold: min_entropy) >= min_entropy
|
28
|
+
end
|
29
|
+
|
30
|
+
def is_weak?(min_entropy: 18)
|
31
|
+
!is_strong?(min_entropy: min_entropy)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the minimum entropy for the password's qwerty locality
|
35
|
+
# adjustments. If a threshhold is specified we will bail
|
36
|
+
# early to avoid unnecessary processing.
|
37
|
+
def adjusted_entropy(entropy_threshhold: 0)
|
38
|
+
revpassword = base_password.reverse
|
39
|
+
min_entropy = [EntropyCalculator.calculate(base_password), EntropyCalculator.calculate(revpassword)].min
|
40
|
+
QWERTY_STRINGS.each do |qwertystr|
|
41
|
+
qpassword = mask_qwerty_strings(base_password, qwertystr)
|
42
|
+
qrevpassword = mask_qwerty_strings(revpassword, qwertystr)
|
43
|
+
if qpassword != base_password
|
44
|
+
numbits = EntropyCalculator.calculate(qpassword)
|
45
|
+
min_entropy = [min_entropy, numbits].min
|
46
|
+
return min_entropy if min_entropy < entropy_threshhold
|
47
|
+
end
|
48
|
+
if qrevpassword != revpassword
|
49
|
+
numbits = EntropyCalculator.calculate(qrevpassword)
|
50
|
+
min_entropy = [min_entropy, numbits].min
|
51
|
+
return min_entropy if min_entropy < entropy_threshhold
|
52
|
+
end
|
53
|
+
end
|
54
|
+
min_entropy
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def mask_qwerty_strings(password, qwerty_string)
|
60
|
+
masked_password = password
|
61
|
+
z = 6
|
62
|
+
begin
|
63
|
+
y = qwerty_string.length - z
|
64
|
+
(0..y).each do |x|
|
65
|
+
str = qwerty_string[x, z].sub('-', '\\-')
|
66
|
+
masked_password = masked_password.sub(str, '*')
|
67
|
+
end
|
68
|
+
z = z - 1
|
69
|
+
end while z > 2
|
70
|
+
masked_password
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rails/railtie'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module StrongPassword
|
2
|
+
class StrengthChecker
|
3
|
+
BASE_ENTROPY = 18
|
4
|
+
|
5
|
+
attr_reader :base_password
|
6
|
+
|
7
|
+
def initialize(password)
|
8
|
+
@base_password = password.dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_weak?(min_entropy: BASE_ENTROPY, use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
|
12
|
+
!is_strong?(min_entropy: min_entropy,
|
13
|
+
use_dictionary: use_dictionary,
|
14
|
+
min_word_length: min_word_length,
|
15
|
+
extra_dictionary_words: extra_dictionary_words)
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_strong?(min_entropy: BASE_ENTROPY, use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
|
19
|
+
weak = (EntropyCalculator.calculate(base_password) < min_entropy) ||
|
20
|
+
(EntropyCalculator.calculate(base_password.downcase) < min_entropy) ||
|
21
|
+
(QwertyAdjuster.new(base_password).is_weak?(min_entropy: min_entropy))
|
22
|
+
if !weak && use_dictionary
|
23
|
+
return DictionaryAdjuster.new(base_password).is_strong?(min_entropy: min_entropy,
|
24
|
+
min_word_length: min_word_length,
|
25
|
+
extra_dictionary_words: extra_dictionary_words)
|
26
|
+
else
|
27
|
+
return !weak
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def calculate_entropy(use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
|
32
|
+
entropies = [EntropyCalculator.calculate(base_password), EntropyCalculator.calculate(base_password.downcase), QwertyAdjuster.new(base_password).adjusted_entropy]
|
33
|
+
entropies << DictionaryAdjuster.new(base_password).adjusted_entropy(min_word_length: min_word_length, extra_dictionary_words: extra_dictionary_words) if use_dictionary
|
34
|
+
entropies.min
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_model/validations'
|
2
|
+
|
3
|
+
require 'strong_password/version'
|
4
|
+
require 'strong_password/nist_bonus_bits'
|
5
|
+
require 'strong_password/entropy_calculator'
|
6
|
+
require 'strong_password/strength_checker'
|
7
|
+
require 'strong_password/password_variants'
|
8
|
+
require 'strong_password/dictionary_adjuster'
|
9
|
+
require 'strong_password/qwerty_adjuster'
|
10
|
+
require 'active_model/validations/password_strength_validator' if defined?(ActiveModel)
|
11
|
+
|
12
|
+
module StrongPassword
|
13
|
+
end
|
14
|
+
|
15
|
+
I18n.load_path << File.dirname(__FILE__) + '/strong_password/locale/en.yml' if defined?(I18n)
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module StrongPassword
|
4
|
+
describe DictionaryAdjuster do
|
5
|
+
describe '#is_strong?' do
|
6
|
+
let(:subject) { DictionaryAdjuster.new('password') }
|
7
|
+
|
8
|
+
it 'returns true if the calculated entropy is >= the minimum' do
|
9
|
+
subject.stub(adjusted_entropy: 18)
|
10
|
+
expect(subject.is_strong?).to be_true
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns false if the calculated entropy is < the minimum' do
|
14
|
+
subject.stub(adjusted_entropy: 17)
|
15
|
+
expect(subject.is_strong?).to be_false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#is_weak?' do
|
20
|
+
let(:subject) { DictionaryAdjuster.new('password') }
|
21
|
+
|
22
|
+
it 'returns the opposite of is_strong?' do
|
23
|
+
subject.stub(is_strong?: true)
|
24
|
+
expect(subject.is_weak?).to be_false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#adjusted_entropy' do
|
29
|
+
before(:each) { NistBonusBits.stub(bonus_bits: 0)}
|
30
|
+
|
31
|
+
it 'checks against all variants of a given password' do
|
32
|
+
password = 'password'
|
33
|
+
adjuster = DictionaryAdjuster.new(password)
|
34
|
+
PasswordVariants.should_receive(:all_variants).with(password).and_return([])
|
35
|
+
adjuster.adjusted_entropy
|
36
|
+
end
|
37
|
+
|
38
|
+
{
|
39
|
+
'bnm,./' => 14, # Qwerty string should not get adjusted by dictionary adjuster
|
40
|
+
'h#e0zbPas' => 19.5, # Random string should not get adjusted by dictionary adjuster
|
41
|
+
'password' => 4, # Adjusts common dictionary words
|
42
|
+
'E_!3password' => 11.5, # Adjusts common dictionary words regardless of placement
|
43
|
+
'h#e0zbPas 32e2i81 password' => 31.3125, # Even if there are multiple words
|
44
|
+
'123456' => 4, # Even if they are also qwerty strings
|
45
|
+
'password123456' => 16 # But only drops the first matched word
|
46
|
+
}.each do |password, bits|
|
47
|
+
it "returns #{bits} for '#{password}'" do
|
48
|
+
expect(DictionaryAdjuster.new(password).adjusted_entropy).to eq(bits)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'allows extra words to be provided as an array' do
|
53
|
+
password = 'mcmanus'
|
54
|
+
base_entropy = EntropyCalculator.calculate(password)
|
55
|
+
expect(DictionaryAdjuster.new(password).adjusted_entropy(extra_dictionary_words: ['mcmanus'])).not_to eq(base_entropy)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'allows minimum word length to be adjusted' do
|
59
|
+
password = '6969'
|
60
|
+
base_entropy = DictionaryAdjuster.new(password).adjusted_entropy
|
61
|
+
# If we increase the min_word_length above the length of the password we should get a higher entropy
|
62
|
+
expect(DictionaryAdjuster.new(password).adjusted_entropy(min_word_length: 6)).not_to be < base_entropy
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module StrongPassword
|
4
|
+
describe EntropyCalculator do
|
5
|
+
describe '.bits' do
|
6
|
+
before(:each) { NistBonusBits.stub(bonus_bits: 0) }
|
7
|
+
{
|
8
|
+
'' => 0,
|
9
|
+
'*' => 4,
|
10
|
+
'**' => 6,
|
11
|
+
'***' => 8,
|
12
|
+
'****' => 10,
|
13
|
+
'*****' => 12,
|
14
|
+
'******' => 14,
|
15
|
+
'*******' => 16,
|
16
|
+
'********' => 18,
|
17
|
+
'*********' => 19.5,
|
18
|
+
'**********' => 21,
|
19
|
+
'***********' => 22.5,
|
20
|
+
'************' => 24,
|
21
|
+
'*************' => 25.5,
|
22
|
+
'**************' => 27,
|
23
|
+
'***************' => 28.5,
|
24
|
+
'****************' => 30,
|
25
|
+
'*****************' => 31.5,
|
26
|
+
'******************' => 33,
|
27
|
+
'*******************' => 34.5,
|
28
|
+
'********************' => 36,
|
29
|
+
'*********************' => 37,
|
30
|
+
'**********************' => 38,
|
31
|
+
'***********************' => 39,
|
32
|
+
'************************' => 40
|
33
|
+
}.each do |password, bits|
|
34
|
+
it "returns #{bits} for #{password.length} characters" do
|
35
|
+
expect(subject.bits(password)).to eq(bits)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.bits_with_repeats_weakened' do
|
41
|
+
before(:each) { NistBonusBits.stub(bonus_bits: 0) }
|
42
|
+
{
|
43
|
+
'' => 0,
|
44
|
+
'*' => 4,
|
45
|
+
'**' => 5.5,
|
46
|
+
'***' => 6.625,
|
47
|
+
'****' => 7.46875,
|
48
|
+
'****a' => 9.46875,
|
49
|
+
'********' => 9.1990966796875
|
50
|
+
}.each do |password, bits|
|
51
|
+
it "returns #{bits} for #{password.length} characters" do
|
52
|
+
expect(subject.bits_with_repeats_weakened(password)).to eq(bits)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns the same value for repeated calls on a password' do
|
57
|
+
password = 'password'
|
58
|
+
initial_value = subject.bits_with_repeats_weakened(password)
|
59
|
+
5.times do
|
60
|
+
expect(subject.bits_with_repeats_weakened(password)).to eq(initial_value)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module StrongPassword
|
4
|
+
describe NistBonusBits do
|
5
|
+
describe '.bonus_bits' do
|
6
|
+
it 'calculates the bonus bits the first time for a given password' do
|
7
|
+
NistBonusBits.reset_bonus_cache!
|
8
|
+
NistBonusBits.should_receive(:calculate_bonus_bits_for).and_return(1)
|
9
|
+
expect(NistBonusBits.bonus_bits('password')).to eq(1)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'caches the bonus bits for a password for later use' do
|
13
|
+
NistBonusBits.reset_bonus_cache!
|
14
|
+
NistBonusBits.stub(calculate_bonus_bits_for: 1)
|
15
|
+
NistBonusBits.bonus_bits('password')
|
16
|
+
NistBonusBits.should_not_receive(:calculate_bonus_bits_for)
|
17
|
+
expect(NistBonusBits.bonus_bits('password')).to eq(1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '.calculate_bonus_bits_for' do
|
22
|
+
{
|
23
|
+
'Ab$9' => 4,
|
24
|
+
'Ab $9' => 6,
|
25
|
+
'A b $ 9' => 7,
|
26
|
+
'Ab$' => 3,
|
27
|
+
'Ab $' => 5,
|
28
|
+
'A b $ c d' => 6,
|
29
|
+
'1!' => -4,
|
30
|
+
'1 !' => -2,
|
31
|
+
'1 ! 2 # 3 $' => -1,
|
32
|
+
'123' => -8,
|
33
|
+
'1 23' => -6,
|
34
|
+
'1 2 3 4 5 6' => -5,
|
35
|
+
'blah' => -2,
|
36
|
+
'blah blah' => 0,
|
37
|
+
'blah blah blah blah' => 1
|
38
|
+
}.each do |password, bonus_bits|
|
39
|
+
it "returns #{bonus_bits} for '#{password}'" do
|
40
|
+
expect(NistBonusBits.calculate_bonus_bits_for(password)).to eq(bonus_bits)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module StrongPassword
|
4
|
+
describe PasswordVariants do
|
5
|
+
describe '.all_variants' do
|
6
|
+
it 'includes the lowercase password' do
|
7
|
+
expect(subject.all_variants("PASSWORD")).to include('password')
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'includes keyboard shift variants' do
|
11
|
+
subject.stub(keyboard_shift_variants: ['foo', 'bar'])
|
12
|
+
expect(subject.all_variants("password")).to include('foo', 'bar')
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'includes leet speak variants' do
|
16
|
+
subject.stub(leet_speak_variants: ['foo', 'bar'])
|
17
|
+
expect(subject.all_variants("password")).to include('foo', 'bar')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not mutate the password' do
|
21
|
+
password = 'PASSWORD'
|
22
|
+
subject.all_variants(password)
|
23
|
+
expect(password).to eq('PASSWORD')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '.keyboard_shift_variants' do
|
28
|
+
it 'returns no variants if password includes only bottom row characters' do
|
29
|
+
expect(subject.keyboard_shift_variants('zxcvbnm,./')).to eq([])
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'maps down-right passwords' do
|
33
|
+
expect(subject.keyboard_shift_variants('qwerty')).to include('asdfgh')
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'includes reversed down-right password' do
|
37
|
+
expect(subject.keyboard_shift_variants('qwerty')).to include('hgfdsa')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'maps down-left passwords' do
|
41
|
+
expect(subject.keyboard_shift_variants('sdfghj')).to include('zxcvbn')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'maps reversed down-left passwords' do
|
45
|
+
expect(subject.keyboard_shift_variants('sdfghj')).to include('nbvcxz')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.leet_speak_variants' do
|
50
|
+
it 'returns no variants if the password includes no leet speak' do
|
51
|
+
expect(subject.leet_speak_variants('password')).to eq([])
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns standard leet speak variants' do
|
55
|
+
expect(subject.leet_speak_variants('p4ssw0rd')).to include('password')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns reversed standard leet speak variants' do
|
59
|
+
expect(subject.leet_speak_variants('p4ssw0rd')).to include('drowssap')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns both i and l variants when given a 1' do
|
63
|
+
expect(subject.leet_speak_variants('h1b0b')).to include('hibob', 'hlbob')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module StrongPassword
|
4
|
+
describe QwertyAdjuster do
|
5
|
+
describe '#is_strong?' do
|
6
|
+
let(:subject) { QwertyAdjuster.new('password') }
|
7
|
+
|
8
|
+
it 'returns true if the calculated entropy is >= the minimum' do
|
9
|
+
subject.stub(adjusted_entropy: 18)
|
10
|
+
expect(subject.is_strong?).to be_true
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns false if the calculated entropy is < the minimum' do
|
14
|
+
subject.stub(adjusted_entropy: 17)
|
15
|
+
expect(subject.is_strong?).to be_false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#is_weak?' do
|
20
|
+
let(:subject) { QwertyAdjuster.new('password') }
|
21
|
+
|
22
|
+
it 'returns the opposite of is_strong?' do
|
23
|
+
subject.stub(is_strong?: true)
|
24
|
+
expect(subject.is_weak?).to be_false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#adjusted_entropy' do
|
29
|
+
before(:each) { NistBonusBits.stub(bonus_bits: 0)}
|
30
|
+
{
|
31
|
+
'qwertyuio' => 5.5,
|
32
|
+
'1234567' => 6,
|
33
|
+
'lkjhgfd' => 6,
|
34
|
+
'0987654321' => 5.5,
|
35
|
+
'zxcvbnm' => 6,
|
36
|
+
'/.,mnbvcx' => 5.5,
|
37
|
+
'password' => 17.5 # Ensure that we don't qwerty-adjust 'password'
|
38
|
+
}.each do |password, bits|
|
39
|
+
it "returns #{bits} for '#{password}'" do
|
40
|
+
expect(QwertyAdjuster.new(password).adjusted_entropy).to eq(bits)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module StrongPassword
|
4
|
+
describe StrengthChecker do
|
5
|
+
context 'with lowered entropy requirement and no dictionary checking' do
|
6
|
+
{
|
7
|
+
'blahblah' => true,
|
8
|
+
'password' => true,
|
9
|
+
'wwwwwwww' => false,
|
10
|
+
'adamruge' => true,
|
11
|
+
'aB$1' => false
|
12
|
+
}.each do |password, strength|
|
13
|
+
it "is_strong? returns #{strength} for '#{password}' with 12 bits of entropy" do
|
14
|
+
expect(StrengthChecker.new(password).is_strong?(min_entropy: 12)).to be(strength)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with lowered entropy requirement and dictionary checking' do
|
20
|
+
{
|
21
|
+
'blahblah' => true,
|
22
|
+
'password' => false,
|
23
|
+
'wwwwwwww' => false,
|
24
|
+
'adamruge' => true,
|
25
|
+
'aB$1' => false
|
26
|
+
}.each do |password, strength|
|
27
|
+
it "is_strong? returns #{strength} for '#{password}' with 12 bits of entropy" do
|
28
|
+
expect(StrengthChecker.new(password).is_strong?(min_entropy: 12, use_dictionary: true)).to be(strength)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with standard entropy requirement and dictionary checking' do
|
34
|
+
{
|
35
|
+
'blahblah' => false,
|
36
|
+
'password' => false,
|
37
|
+
'wwwwwwww' => false,
|
38
|
+
'adamruge' => false,
|
39
|
+
'aB$1' => false,
|
40
|
+
'correct horse battery staple' => true
|
41
|
+
}.each do |password, strength|
|
42
|
+
it "is_strong? returns #{strength} for '#{password}' with standard bits of entropy" do
|
43
|
+
expect(StrengthChecker.new(password).is_strong?(use_dictionary: true)).to be(strength)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with crazy entropy requirement and dictionary checking' do
|
49
|
+
{
|
50
|
+
'blahblah' => false,
|
51
|
+
'password' => false,
|
52
|
+
'wwwwwwww' => false,
|
53
|
+
'adamruge' => false,
|
54
|
+
'aB$1' => false,
|
55
|
+
'correct horse battery staple' => false,
|
56
|
+
'c0rr#ct h0rs3 Batt$ry st@pl3 is Gr34t' => true
|
57
|
+
}.each do |password, strength|
|
58
|
+
it "is_strong? returns #{strength} for '#{password}' with standard bits of entropy" do
|
59
|
+
expect(StrengthChecker.new(password).is_strong?(min_entropy: 40, use_dictionary: true)).to be(strength)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|