strong_password 0.0.4 → 0.0.10

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