strong_password 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|