strong_password 0.0.3 → 0.0.9

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.
@@ -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)
@@ -73,4 +73,4 @@ module StrongPassword
73
73
  end
74
74
  end
75
75
  end
76
- end
76
+ end
@@ -2,4 +2,4 @@ en:
2
2
  errors:
3
3
  messages:
4
4
  password:
5
- password_strength: "%{attribute} is too weak"
5
+ password_strength: "is too weak"
@@ -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
@@ -1,6 +1,8 @@
1
1
  module StrongPassword
2
2
  module PasswordVariants
3
- LEET_SPEAK_1 = {
3
+ LEET_SPEAK_REGEXP = /[\@\!\$1234567890]/
4
+
5
+ LEET_SPEAK = {
4
6
  "@" => "a",
5
7
  "!" => "i",
6
8
  "$" => "s",
@@ -16,37 +18,9 @@ module StrongPassword
16
18
  "0" => "o"
17
19
  }
18
20
 
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
- }
21
+ LEET_SPEAK_ALT = LEET_SPEAK.dup.merge!("1" => "l")
34
22
 
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
- }
23
+ BOTTOM_ROW_REGEXP = /[zxcvbnm,\.\/\<\>\?]/
50
24
 
51
25
  KEYBOARDMAP_DOWNRIGHT = {
52
26
  "a" => "z",
@@ -105,10 +79,10 @@ module StrongPassword
105
79
  "p" => "l",
106
80
  "-" => "p"
107
81
  }
108
-
82
+
109
83
  # Returns all variants of a given password including the password itself
110
84
  def self.all_variants(password)
111
- passwords = [password.dup.downcase]
85
+ passwords = [password.downcase]
112
86
  passwords += keyboard_shift_variants(password)
113
87
  passwords += leet_speak_variants(password)
114
88
  passwords.uniq
@@ -116,38 +90,27 @@ module StrongPassword
116
90
 
117
91
  # Returns all keyboard shifted variants of a given password
118
92
  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
93
+ password = password.downcase
126
94
 
127
- variant = password.tr(KEYBOARDMAP_DOWNLEFT.keys.join, KEYBOARDMAP_DOWNLEFT.values.join)
128
- variants << variant
129
- variants << variant.reverse
130
- end
131
- variants
95
+ return [] if password.match(BOTTOM_ROW_REGEXP)
96
+ variants(password, KEYBOARDMAP_DOWNRIGHT, KEYBOARDMAP_DOWNLEFT)
132
97
  end
133
98
 
134
99
  # Returns all leet speak variants of a given password
135
100
  def self.leet_speak_variants(password)
136
- password = password.dup.downcase
137
- variants = []
101
+ password = password.downcase
138
102
 
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
103
+ return [] if !password.match(LEET_SPEAK_REGEXP)
104
+ variants(password, LEET_SPEAK, LEET_SPEAK_ALT)
105
+ end
106
+
107
+ private
144
108
 
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
109
+ def self.variants(password, *mappings)
110
+ mappings.flat_map do |map|
111
+ variant = password.tr(map.keys.join, map.values.join)
112
+ [variant, variant.reverse]
149
113
  end
150
- variants
151
114
  end
152
115
  end
153
- end
116
+ end
@@ -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.dup.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.3"
2
+ VERSION = '0.0.9'.freeze
3
3
  end
@@ -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