strong_password 0.0.5 → 0.0.6
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/Gemfile +4 -1
- data/README.md +1 -1
- data/lib/active_model/validations/password_strength_validator.rb +1 -1
- data/lib/strong_password/dictionary_adjuster.rb +1 -1
- data/lib/strong_password/entropy_calculator.rb +8 -8
- data/lib/strong_password/nist_bonus_bits.rb +5 -5
- data/lib/strong_password/qwerty_adjuster.rb +8 -8
- data/lib/strong_password/strength_checker.rb +8 -5
- data/lib/strong_password/version.rb +1 -1
- data/spec/spec_helper.rb +5 -2
- data/spec/strong_password/entropy_calculator_spec.rb +3 -3
- data/spec/strong_password/nist_bonus_bits_spec.rb +2 -2
- data/spec/strong_password/password_variants_spec.rb +12 -12
- data/spec/strong_password/strength_checker_spec.rb +24 -4
- data/strong_password.gemspec +1 -1
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0506c07961d8cf54ee30542579493af60a4a508c176f7de7c0131d8082bcdf35
|
4
|
+
data.tar.gz: 132ea4c4bc8a96f44b687a342f418ef9c183efb696618cf6b6a7f14a3558c590
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9342ba93d158425cb8623662341f43fda16c33102590c846571767f09aa8def4a34f4d0d03eb150ea468b3f3c2de58a4cdf2f57c29367fbbc402a3e797b4f893
|
7
|
+
data.tar.gz: 5ba21b3b35527fc07fe5744dc9560e083fe0844a316c88ef22249aeacea0f77ec1b3082a6fb20b4eb6cc7f4cabb63560433023c1c438a73a28e60ec46e64b4aa
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -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
|
@@ -16,21 +16,21 @@ module StrongPassword
|
|
16
16
|
"014725836914702583697894561230258/369*+-*/",
|
17
17
|
"abcdefghijklmnopqrstuvwxyz"
|
18
18
|
]
|
19
|
-
|
19
|
+
|
20
20
|
attr_reader :base_password
|
21
|
-
|
21
|
+
|
22
22
|
def initialize(password)
|
23
23
|
@base_password = password.downcase
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
def is_strong?(min_entropy: 18)
|
27
27
|
adjusted_entropy(entropy_threshhold: min_entropy) >= min_entropy
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
def is_weak?(min_entropy: 18)
|
31
31
|
!is_strong?(min_entropy: min_entropy)
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
# Returns the minimum entropy for the password's qwerty locality
|
35
35
|
# adjustments. If a threshhold is specified we will bail
|
36
36
|
# early to avoid unnecessary processing.
|
@@ -53,9 +53,9 @@ module StrongPassword
|
|
53
53
|
end
|
54
54
|
min_entropy
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
private
|
58
|
-
|
58
|
+
|
59
59
|
def mask_qwerty_strings(password, qwerty_string)
|
60
60
|
masked_password = password
|
61
61
|
z = 6
|
@@ -70,4 +70,4 @@ module StrongPassword
|
|
70
70
|
masked_password
|
71
71
|
end
|
72
72
|
end
|
73
|
-
end
|
73
|
+
end
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module StrongPassword
|
2
2
|
class StrengthChecker
|
3
3
|
BASE_ENTROPY = 18
|
4
|
-
|
4
|
+
PASSWORD_LIMIT = 1_000
|
5
|
+
EXTRA_WORDS_LIMIT = 1_000
|
6
|
+
|
5
7
|
attr_reader :base_password
|
6
8
|
|
7
9
|
def initialize(password)
|
8
|
-
@base_password = password.dup
|
10
|
+
@base_password = password.dup[0...PASSWORD_LIMIT]
|
9
11
|
end
|
10
|
-
|
12
|
+
|
11
13
|
def is_weak?(min_entropy: BASE_ENTROPY, use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
|
12
14
|
!is_strong?(min_entropy: min_entropy,
|
13
15
|
use_dictionary: use_dictionary,
|
@@ -27,11 +29,12 @@ module StrongPassword
|
|
27
29
|
return !weak
|
28
30
|
end
|
29
31
|
end
|
30
|
-
|
32
|
+
|
31
33
|
def calculate_entropy(use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
|
34
|
+
extra_dictionary_words.collect! { |w| w[0...EXTRA_WORDS_LIMIT] }
|
32
35
|
entropies = [EntropyCalculator.calculate(base_password), EntropyCalculator.calculate(base_password.downcase), QwertyAdjuster.new(base_password).adjusted_entropy]
|
33
36
|
entropies << DictionaryAdjuster.new(base_password).adjusted_entropy(min_word_length: min_word_length, extra_dictionary_words: extra_dictionary_words) if use_dictionary
|
34
37
|
entropies.min
|
35
38
|
end
|
36
39
|
end
|
37
|
-
end
|
40
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
require
|
2
|
-
|
1
|
+
require 'simplecov'
|
2
|
+
require 'simplecov-console'
|
3
|
+
SimpleCov.formatter = SimpleCov::Formatter::Console
|
4
|
+
SimpleCov.start
|
5
|
+
|
3
6
|
require 'bundler/setup'
|
4
7
|
require 'pry'
|
5
8
|
require 'active_model'
|
@@ -36,9 +36,9 @@ module StrongPassword
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
describe '.bits_with_repeats_weakened' do
|
41
|
-
before(:each) { NistBonusBits.
|
41
|
+
before(:each) { allow(NistBonusBits).to receive(:bonus_bits).and_return(0) }
|
42
42
|
{
|
43
43
|
'' => 0,
|
44
44
|
'*' => 4,
|
@@ -52,7 +52,7 @@ module StrongPassword
|
|
52
52
|
expect(subject.bits_with_repeats_weakened(password)).to eq(bits)
|
53
53
|
end
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
it 'returns the same value for repeated calls on a password' do
|
57
57
|
password = 'password'
|
58
58
|
initial_value = subject.bits_with_repeats_weakened(password)
|
@@ -8,7 +8,7 @@ module StrongPassword
|
|
8
8
|
NistBonusBits.should_receive(:calculate_bonus_bits_for).and_return(1)
|
9
9
|
expect(NistBonusBits.bonus_bits('password')).to eq(1)
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
it 'caches the bonus bits for a password for later use' do
|
13
13
|
NistBonusBits.reset_bonus_cache!
|
14
14
|
NistBonusBits.stub(calculate_bonus_bits_for: 1)
|
@@ -17,7 +17,7 @@ module StrongPassword
|
|
17
17
|
expect(NistBonusBits.bonus_bits('password')).to eq(1)
|
18
18
|
end
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
describe '.calculate_bonus_bits_for' do
|
22
22
|
{
|
23
23
|
'Ab$9' => 4,
|
@@ -6,59 +6,59 @@ module StrongPassword
|
|
6
6
|
it 'includes the lowercase password' do
|
7
7
|
expect(subject.all_variants("PASSWORD")).to include('password')
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
it 'includes keyboard shift variants' do
|
11
11
|
subject.stub(keyboard_shift_variants: ['foo', 'bar'])
|
12
12
|
expect(subject.all_variants("password")).to include('foo', 'bar')
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
it 'includes leet speak variants' do
|
16
16
|
subject.stub(leet_speak_variants: ['foo', 'bar'])
|
17
17
|
expect(subject.all_variants("password")).to include('foo', 'bar')
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
it 'does not mutate the password' do
|
21
21
|
password = 'PASSWORD'
|
22
22
|
subject.all_variants(password)
|
23
23
|
expect(password).to eq('PASSWORD')
|
24
24
|
end
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
describe '.keyboard_shift_variants' do
|
28
28
|
it 'returns no variants if password includes only bottom row characters' do
|
29
29
|
expect(subject.keyboard_shift_variants('zxcvbnm,./')).to eq([])
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
it 'maps down-right passwords' do
|
33
33
|
expect(subject.keyboard_shift_variants('qwerty')).to include('asdfgh')
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
it 'includes reversed down-right password' do
|
37
37
|
expect(subject.keyboard_shift_variants('qwerty')).to include('hgfdsa')
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
it 'maps down-left passwords' do
|
41
41
|
expect(subject.keyboard_shift_variants('sdfghj')).to include('zxcvbn')
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
it 'maps reversed down-left passwords' do
|
45
45
|
expect(subject.keyboard_shift_variants('sdfghj')).to include('nbvcxz')
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
describe '.leet_speak_variants' do
|
50
50
|
it 'returns no variants if the password includes no leet speak' do
|
51
51
|
expect(subject.leet_speak_variants('password')).to eq([])
|
52
52
|
end
|
53
|
-
|
53
|
+
|
54
54
|
it 'returns standard leet speak variants' do
|
55
55
|
expect(subject.leet_speak_variants('p4ssw0rd')).to include('password')
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
it 'returns reversed standard leet speak variants' do
|
59
59
|
expect(subject.leet_speak_variants('p4ssw0rd')).to include('drowssap')
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
62
|
it 'returns both i and l variants when given a 1' do
|
63
63
|
expect(subject.leet_speak_variants('h1b0b')).to include('hibob', 'hlbob')
|
64
64
|
end
|
@@ -15,7 +15,7 @@ module StrongPassword
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
context 'with lowered entropy requirement and dictionary checking' do
|
20
20
|
{
|
21
21
|
'blahblah' => true,
|
@@ -30,7 +30,7 @@ module StrongPassword
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
context 'with standard entropy requirement and dictionary checking' do
|
35
35
|
{
|
36
36
|
'blahblah' => false,
|
@@ -45,7 +45,7 @@ module StrongPassword
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
context 'with crazy entropy requirement and dictionary checking' do
|
50
50
|
{
|
51
51
|
'blahblah' => false,
|
@@ -61,5 +61,25 @@ module StrongPassword
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
64
|
+
|
65
|
+
context 'with long password' do
|
66
|
+
let(:strength_checker) { StrengthChecker.new("ba"*500_000) }
|
67
|
+
it 'should be truncated' do
|
68
|
+
expect(strength_checker.instance_variable_get(:@base_password).length).to eq StrengthChecker::PASSWORD_LIMIT
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'with long extra words' do
|
73
|
+
let(:strength_checker) { StrengthChecker.new("$tr0NgP4s$w0rd91d£") }
|
74
|
+
let(:exta_limit) { StrengthChecker::EXTRA_WORDS_LIMIT }
|
75
|
+
it 'should be truncated' do
|
76
|
+
expect_any_instance_of(DictionaryAdjuster).to receive(:adjusted_entropy).with({
|
77
|
+
min_word_length: 4,
|
78
|
+
extra_dictionary_words:
|
79
|
+
["a"*StrengthChecker::EXTRA_WORDS_LIMIT, "b"*StrengthChecker::EXTRA_WORDS_LIMIT, "c"*10]
|
80
|
+
}).and_call_original
|
81
|
+
strength_checker.calculate_entropy(use_dictionary: true, extra_dictionary_words: ["a"*1_000_000, "b"*10_000_000, "c"*10])
|
82
|
+
end
|
83
|
+
end
|
64
84
|
end
|
65
|
-
end
|
85
|
+
end
|
data/strong_password.gemspec
CHANGED
@@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
22
|
spec.add_development_dependency 'rake'
|
23
|
-
spec.add_development_dependency 'rspec', '~>
|
23
|
+
spec.add_development_dependency 'rspec', '~> 3.8'
|
24
24
|
spec.add_development_dependency 'pry'
|
25
25
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_password
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian McManus
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '3.8'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '3.8'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: pry
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -120,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
120
|
version: '0'
|
121
121
|
requirements: []
|
122
122
|
rubyforge_project:
|
123
|
-
rubygems_version: 2.
|
123
|
+
rubygems_version: 2.7.6
|
124
124
|
signing_key:
|
125
125
|
specification_version: 4
|
126
126
|
summary: StrongPassword adds a class to check password strength and a validator for
|
@@ -134,4 +134,3 @@ test_files:
|
|
134
134
|
- spec/strong_password/qwerty_adjuster_spec.rb
|
135
135
|
- spec/strong_password/strength_checker_spec.rb
|
136
136
|
- spec/validation/strength_validator_spec.rb
|
137
|
-
has_rdoc:
|