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.
@@ -1,73 +1,75 @@
1
1
  module StrongPassword
2
2
  class QwertyAdjuster
3
3
  QWERTY_STRINGS = [
4
- "1234567890-",
5
- "qwertyuiop",
6
- "asdfghjkl;",
7
- "zxcvbnm,./",
4
+ '1234567890-',
5
+ 'qwertyuiop',
6
+ 'asdfghjkl;',
7
+ 'zxcvbnm,./',
8
8
  "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]:?_{\"+}",
9
- "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p",
9
+ '1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p',
10
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.downcase
11
+ 'qazwsxedcrfvtgbyhnujmikolp',
12
+ ']"/=[;.-pl,0okm9ijn8uhb7ygv6tfc5rdx4esz3wa2q1',
13
+ 'pl0okm9ijn8uhb7ygv6tfc5rdx4esz3wa2q1',
14
+ ']"/[;.pl,okmijnuhbygvtfcrdxeszwaq',
15
+ 'plokmijnuhbygvtfcrdxeszwaq',
16
+ '014725836914702583697894561230258/369*+-*/',
17
+ 'abcdefghijklmnopqrstuvwxyz'
18
+ ].freeze
19
+
20
+ attr_reader :min_entropy, :entropy_threshhold
21
+
22
+ def initialize(min_entropy: 18, entropy_threshhold: min_entropy)
23
+ @min_entropy = min_entropy
24
+ @entropy_threshhold = entropy_threshhold
24
25
  end
25
-
26
- def is_strong?(min_entropy: 18)
27
- adjusted_entropy(entropy_threshhold: min_entropy) >= min_entropy
26
+
27
+ def is_strong?(base_password)
28
+ adjusted_entropy(base_password) >= min_entropy
28
29
  end
29
-
30
- def is_weak?(min_entropy: 18)
31
- !is_strong?(min_entropy: min_entropy)
30
+
31
+ def is_weak?(base_password)
32
+ !is_strong?(base_password)
32
33
  end
33
-
34
+
34
35
  # Returns the minimum entropy for the password's qwerty locality
35
36
  # adjustments. If a threshhold is specified we will bail
36
37
  # early to avoid unnecessary processing.
37
- def adjusted_entropy(entropy_threshhold: 0)
38
+ def adjusted_entropy(base_password)
38
39
  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
40
+ lowest_entropy = [EntropyCalculator.calculate(base_password), EntropyCalculator.calculate(revpassword)].min
41
+ # If our entropy is already lower than we care about then there's no reason to look further.
42
+ return lowest_entropy if lowest_entropy < entropy_threshhold
43
+
44
+ qpassword = mask_qwerty_strings(base_password)
45
+ lowest_entropy = [lowest_entropy, EntropyCalculator.calculate(qpassword)].min if qpassword != base_password
46
+ # Bail early if our entropy on the base password's masked qwerty value is less than our threshold.
47
+ return lowest_entropy if lowest_entropy < entropy_threshhold
48
+
49
+ qrevpassword = mask_qwerty_strings(revpassword)
50
+ lowest_entropy = [lowest_entropy, EntropyCalculator.calculate(qrevpassword)].min if qrevpassword != revpassword
51
+ lowest_entropy
55
52
  end
56
-
57
- private
58
-
59
- def mask_qwerty_strings(password, qwerty_string)
60
- masked_password = password
61
- z = 6
62
- begin
53
+
54
+ private
55
+
56
+ def all_qwerty_strings
57
+ @all_qwerty_strings ||= Regexp.union(QWERTY_STRINGS.flat_map do |qwerty_string|
58
+ gen_qw_strings(qwerty_string)
59
+ end)
60
+ end
61
+
62
+ def gen_qw_strings(qwerty_string)
63
+ 6.downto(3).flat_map do |z|
63
64
  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, '*')
65
+ (0..y).map do |x|
66
+ qwerty_string[x, z].sub('-', '\\-')
67
67
  end
68
- z = z - 1
69
- end while z > 2
70
- masked_password
68
+ end
69
+ end
70
+
71
+ def mask_qwerty_strings(password)
72
+ password.gsub(all_qwerty_strings, '*')
71
73
  end
72
74
  end
73
- end
75
+ end
@@ -1 +1 @@
1
- require 'rails/railtie'
1
+ require 'rails/railtie' if defined?(Rails)
@@ -1,37 +1,58 @@
1
1
  module StrongPassword
2
2
  class StrengthChecker
3
3
  BASE_ENTROPY = 18
4
-
5
- attr_reader :base_password
4
+ PASSWORD_LIMIT = 1_000
5
+ EXTRA_WORDS_LIMIT = 1_000
6
6
 
7
- def initialize(password)
8
- @base_password = password.dup
7
+ attr_reader :min_entropy, :use_dictionary, :min_word_length, :extra_dictionary_words
8
+
9
+ def initialize(min_entropy: BASE_ENTROPY, use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
10
+ @min_entropy = min_entropy
11
+ @use_dictionary = use_dictionary
12
+ @min_word_length = min_word_length
13
+ @extra_dictionary_words = extra_dictionary_words
9
14
  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)
15
+
16
+ def is_weak?(password)
17
+ !is_strong?(password)
16
18
  end
17
19
 
18
- def is_strong?(min_entropy: BASE_ENTROPY, use_dictionary: false, min_word_length: 4, extra_dictionary_words: [])
20
+ def is_strong?(password)
21
+ base_password = password.dup[0...PASSWORD_LIMIT]
19
22
  weak = (EntropyCalculator.calculate(base_password) < min_entropy) ||
20
23
  (EntropyCalculator.calculate(base_password.downcase) < min_entropy) ||
21
- (QwertyAdjuster.new(base_password).is_weak?(min_entropy: min_entropy))
24
+ (qwerty_adjuster.is_weak?(base_password))
22
25
  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
+ return dictionary_adjuster.is_strong?(base_password)
26
27
  else
27
28
  return !weak
28
29
  end
29
30
  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
31
+
32
+ def calculate_entropy(password)
33
+ base_password = password.dup[0...PASSWORD_LIMIT]
34
+ extra_dictionary_words.collect! { |w| w[0...EXTRA_WORDS_LIMIT] }
35
+ entropies = [
36
+ EntropyCalculator.calculate(base_password),
37
+ EntropyCalculator.calculate(base_password.downcase),
38
+ qwerty_adjuster.adjusted_entropy(base_password)
39
+ ]
40
+ entropies << dictionary_adjuster.adjusted_entropy(base_password) if use_dictionary
34
41
  entropies.min
35
42
  end
43
+
44
+ private
45
+
46
+ def qwerty_adjuster
47
+ @qwerty_adjuster ||= QwertyAdjuster.new(min_entropy: min_entropy)
48
+ end
49
+
50
+ def dictionary_adjuster
51
+ @dictionary_adjuster ||= DictionaryAdjuster.new(
52
+ min_word_length: min_word_length,
53
+ extra_dictionary_words: extra_dictionary_words,
54
+ min_entropy: min_entropy
55
+ )
56
+ end
36
57
  end
37
- end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongPassword
2
- VERSION = "0.0.4"
2
+ VERSION = '0.0.10'.freeze
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,14 @@
1
+ require 'simplecov'
2
+ require 'simplecov-console'
3
+ SimpleCov.formatter = SimpleCov::Formatter::Console
4
+ SimpleCov.start
5
+
1
6
  require 'bundler/setup'
2
- require 'strong_password'
7
+ require 'pry'
3
8
  require 'active_model'
9
+ require 'strong_password'
4
10
 
5
11
  RSpec.configure do |config|
6
12
  config.expect_with(:rspec) {|c| c.syntax = :expect}
7
13
  config.order = :random
8
- end
14
+ end
@@ -3,36 +3,36 @@ require 'spec_helper'
3
3
  module StrongPassword
4
4
  describe DictionaryAdjuster do
5
5
  describe '#is_strong?' do
6
- let(:subject) { DictionaryAdjuster.new('password') }
6
+ let(:subject) { DictionaryAdjuster.new }
7
7
 
8
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
9
+ allow(subject).to receive_messages(adjusted_entropy: 18)
10
+ expect(subject.is_strong?('password')).to be_truthy
11
11
  end
12
12
 
13
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
14
+ allow(subject).to receive_messages(adjusted_entropy: 17)
15
+ expect(subject.is_strong?('password')).to be_falsey
16
16
  end
17
17
  end
18
18
 
19
19
  describe '#is_weak?' do
20
- let(:subject) { DictionaryAdjuster.new('password') }
20
+ let(:subject) { DictionaryAdjuster.new }
21
21
 
22
22
  it 'returns the opposite of is_strong?' do
23
- subject.stub(is_strong?: true)
24
- expect(subject.is_weak?).to be_false
23
+ allow(subject).to receive_messages(is_strong?: true)
24
+ expect(subject.is_weak?('password')).to be_falsey
25
25
  end
26
26
  end
27
27
 
28
28
  describe '#adjusted_entropy' do
29
- before(:each) { NistBonusBits.stub(bonus_bits: 0)}
29
+ before(:each) { allow(NistBonusBits).to receive_messages(bonus_bits: 0) }
30
30
 
31
31
  it 'checks against all variants of a given password' do
32
32
  password = 'password'
33
- adjuster = DictionaryAdjuster.new(password)
34
- PasswordVariants.should_receive(:all_variants).with(password).and_return([])
35
- adjuster.adjusted_entropy
33
+ adjuster = DictionaryAdjuster.new
34
+ expect(PasswordVariants).to receive(:all_variants).with(password).and_return([])
35
+ adjuster.adjusted_entropy(password)
36
36
  end
37
37
 
38
38
  {
@@ -47,22 +47,22 @@ module StrongPassword
47
47
  'asdf[]asdf' => 16 # Doesn't break with []s
48
48
  }.each do |password, bits|
49
49
  it "returns #{bits} for '#{password}'" do
50
- expect(DictionaryAdjuster.new(password).adjusted_entropy).to eq(bits)
50
+ expect(DictionaryAdjuster.new.adjusted_entropy(password)).to eq(bits)
51
51
  end
52
52
  end
53
53
 
54
54
  it 'allows extra words to be provided as an array' do
55
- password = 'mcmanus'
55
+ password = 'administratorWEQ@123'
56
56
  base_entropy = EntropyCalculator.calculate(password)
57
- expect(DictionaryAdjuster.new(password).adjusted_entropy(extra_dictionary_words: ['mcmanus'])).not_to eq(base_entropy)
57
+ expect(DictionaryAdjuster.new(extra_dictionary_words: ['administrator']).adjusted_entropy(password)).not_to eq(base_entropy)
58
58
  end
59
59
 
60
60
  it 'allows minimum word length to be adjusted' do
61
61
  password = '6969'
62
- base_entropy = DictionaryAdjuster.new(password).adjusted_entropy
62
+ base_entropy = DictionaryAdjuster.new.adjusted_entropy(password)
63
63
  # If we increase the min_word_length above the length of the password we should get a higher entropy
64
- expect(DictionaryAdjuster.new(password).adjusted_entropy(min_word_length: 6)).not_to be < base_entropy
64
+ expect(DictionaryAdjuster.new(min_word_length: 6).adjusted_entropy(password)).not_to be < base_entropy
65
65
  end
66
66
  end
67
67
  end
68
- end
68
+ end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  module StrongPassword
4
4
  describe EntropyCalculator do
5
5
  describe '.bits' do
6
- before(:each) { NistBonusBits.stub(bonus_bits: 0) }
6
+ before(:each) { allow(NistBonusBits).to receive_messages(bonus_bits: 0) }
7
7
  {
8
8
  '' => 0,
9
9
  '*' => 4,
@@ -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.stub(bonus_bits: 0) }
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)
@@ -5,19 +5,19 @@ module StrongPassword
5
5
  describe '.bonus_bits' do
6
6
  it 'calculates the bonus bits the first time for a given password' do
7
7
  NistBonusBits.reset_bonus_cache!
8
- NistBonusBits.should_receive(:calculate_bonus_bits_for).and_return(1)
8
+ expect(NistBonusBits).to 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
- NistBonusBits.stub(calculate_bonus_bits_for: 1)
14
+ allow(NistBonusBits).to receive_messages(calculate_bonus_bits_for: 1)
15
15
  NistBonusBits.bonus_bits('password')
16
- NistBonusBits.should_not_receive(:calculate_bonus_bits_for)
16
+ expect(NistBonusBits).not_to receive(:calculate_bonus_bits_for)
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
- subject.stub(keyboard_shift_variants: ['foo', 'bar'])
11
+ allow(subject).to receive_messages(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
- subject.stub(leet_speak_variants: ['foo', 'bar'])
16
+ allow(subject).to receive_messages(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