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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2096a893ba8dd706abcd55dca27bef4595757b44
4
- data.tar.gz: 3223d986cc149971258c53a9bf4bc85a49cf13ff
2
+ SHA256:
3
+ metadata.gz: 4559a04254bce83f7f49ed741cacc81df19a55ca3b7512ffb8fbd63da5ec0a57
4
+ data.tar.gz: 2164536b2690a5946796f874b00db0a80228bb72c19f3d3437625a332a792fa9
5
5
  SHA512:
6
- metadata.gz: 6670c0fa8982bee8acaec5fda404560ab55860fe76ccbe369e4a048297035e625a1040c6699c138d444f62cf3d6cb2d7eaf382b7154f0d7e490c5295ce331509
7
- data.tar.gz: 0dab918c62d3fb5e39996878a67ae61a7a6275ffd141f8a7f700d9685ea1aed6a07a02cb34c168131149ec73f72a7fc0630be152a699a0dd1bc02b477bb13221
6
+ metadata.gz: 75dbfd339fcf1c8a4082ff02bf5f8326a0299fcb600a93e2126e10a409abd3008002c5f705537710f40e9c9989d29fc3feb8ffa22b70c121fb81c60f363f9f23
7
+ data.tar.gz: ea1905e903579dc290b544e99476bbc88585cdff81c45c6e492969978bc465e4ce2e59e160a64c96c05fa16ebd98efc995d60bc196f3e30b5bcb53e289008d72
data/.gitignore CHANGED
@@ -15,3 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .ruby-gemset
19
+ .ruby-version
data/.travis.yml ADDED
@@ -0,0 +1,17 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ - 2.1.7
5
+ - ruby-head
6
+ env:
7
+ - "RAILS_VERSION=4.2"
8
+ - "RAILS_VERSION=4.1"
9
+ - "RAILS_VERSION=4.0"
10
+ - "RAILS_VERSION=3.2"
11
+ - "RAILS_VERSION=3.1"
12
+ - "RAILS_VERSION=3.0"
13
+ - "RAILS_VERSION=master"
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: ruby-head
17
+ - env: "RAILS_VERSION=master"
data/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ ## v0.0.10
2
+ * jfabre - Remove ruby 2.7 warning
3
+ ## v0.0.8
4
+ This release breaks backwards compatibility if you are using the lib components
5
+ directly. If you are using the ActiveModel validations in a Rails project then
6
+ you should be unaffected.
7
+
8
+ The major changes when used directly are that you now initialize the objects
9
+ with the various strength options instead of the password and then provide the
10
+ password to the various methods. The README section on standalone usage has
11
+ been updated to reflect this change.
12
+
13
+ * jacobat - Performance improvements
14
+ * jacksenechal - Updated README
15
+
1
16
  ## v0.0.4
2
17
  * peterkovacs - Increase size of password dictionary
3
18
  * peterkovacs - Swapped in a cleaner, more efficient comparison algorithm
@@ -7,4 +22,4 @@
7
22
 
8
23
  ## v0.0.1
9
24
 
10
- * Initial release
25
+ * Initial release
data/Gemfile CHANGED
@@ -1,8 +1,9 @@
1
1
  source 'https://rubygems.org'
2
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
2
3
 
3
4
  gemspec
4
5
 
5
- version = ENV["RAILS_VERSION"] || "3.2"
6
+ version = ENV["RAILS_VERSION"] || "4.2"
6
7
 
7
8
  rails = case version
8
9
  when "master"
@@ -11,4 +12,8 @@ else
11
12
  "~> #{version}.0"
12
13
  end
13
14
 
14
- gem "rails", rails
15
+ gem "rails", rails
16
+ group :test do
17
+ gem "simplecov", require: false
18
+ gem "simplecov-console", require: false
19
+ end
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ [![Build Status](https://travis-ci.org/bdmac/strong_password.svg?branch=master)](https://travis-ci.org/bdmac/strong_password)
2
+ [![Code Climate](https://codeclimate.com/github/bdmac/strong_password/badges/gpa.svg)](https://codeclimate.com/github/bdmac/strong_password)
3
+ [![Test Coverage](https://codeclimate.com/github/bdmac/strong_password/badges/coverage.svg)](https://codeclimate.com/github/bdmac/strong_password/coverage)
4
+
1
5
  # StrongPassword
2
6
 
3
7
  StrongPassword provides entropy-based password strength checking for your apps.
@@ -15,7 +19,9 @@ NOTE: StrongPassword requires the use of Ruby 2.0. Upgrade if you haven't alrea
15
19
 
16
20
  Add this line to your application's Gemfile:
17
21
 
18
- gem 'strong_password', '~> 0.0.3'
22
+ ```ruby
23
+ gem 'strong_password', '~> 0.0.9'
24
+ ```
19
25
 
20
26
  And then execute:
21
27
 
@@ -61,15 +67,15 @@ class User
61
67
  end
62
68
  ```
63
69
 
64
- The default validation message is "%{attribute} is too weak". If you would like to customize that you can override it in your locale file
65
- by setting a value for this key:
70
+ The default validation message is "is too weak". Rails will display a field of `:password` having too weak of a password with `full_error_messages` as "Password is too weak". If you would like to customize the error message, you can override it in your locale file by setting a value for this key:
71
+
66
72
 
67
73
  ```yml
68
74
  en:
69
75
  errors:
70
76
  messages:
71
77
  password:
72
- password_strength: "%{attribute} is a terrible password, try again!"
78
+ password_strength: "is a terrible password, try again!"
73
79
  ```
74
80
 
75
81
  ### Standalone
@@ -77,19 +83,23 @@ en:
77
83
  StrongPassword can also be used standalone if you need to. There are a few helper methods for determining whether a
78
84
  password is strong or not. You can also directly access the entropy calculations if you want.
79
85
 
80
- ```text
81
- 2.0.0p0 :001 > checker = StrongPassword::StrengthChecker.new('password')
82
- => #<StrongPassword::StrengthChecker:0x007fcd7c939b48 @base_password="password">
83
- 2.0.0p0 :002 > checker.is_strong?
84
- => false
85
- 2.0.0p0 :003 > checker.is_weak?
86
- => true
87
- 2.0.0p0 :004 > checker.is_strong?(min_entropy: 2)
88
- => true
89
- 2.0.0p0 :005 > checker.calculate_entropy
90
- => 15.5
91
- 2.0.0p0 :006 > checker.calculate_entropy(use_dictionary: true)
92
- => 2
86
+ ```console
87
+ irb(main):004:0> checker = StrongPassword::StrengthChecker.new
88
+ => #<StrongPassword::StrengthChecker:0x00007f985509db30 @min_entropy=18, @use_dictionary=false, @min_word_length=4, @extra_dictionary_words=[]>
89
+ irb(main):005:0> checker.is_strong?("password")
90
+ => false
91
+ irb(main):006:0> checker.is_weak?("password")
92
+ => true
93
+ irb(main):007:0> checker = StrongPassword::StrengthChecker.new(min_entropy: 2)
94
+ => #<StrongPassword::StrengthChecker:0x00007f9855147bd0 @min_entropy=2, @use_dictionary=false, @min_word_length=4, @extra_dictionary_words=[]>
95
+ irb(main):008:0> checker.calculate_entropy("password")
96
+ => 15.5
97
+ irb(main):009:0> checker.is_strong?("password")
98
+ => true
99
+ irb(main):010:0> checker = StrongPassword::StrengthChecker.new(use_dictionary: true)
100
+ => #<StrongPassword::StrengthChecker:0x00007f98550ee008 @min_entropy=18, @use_dictionary=true, @min_word_length=4, @extra_dictionary_words=[]>
101
+ irb(main):011:0> checker.calculate_entropy("password")
102
+ => 2
93
103
  ```
94
104
 
95
105
  ## Details
@@ -141,17 +151,21 @@ disallowed by the strength checker.
141
151
 
142
152
  ## Todo
143
153
 
144
- 1. Allow the dictionary of common words to be specified by the user. This is currently hard-coded to
145
- the 500 most common passwords. That also means it's not a true "dictionary" check...
146
- 2. Add a common password adjuster that basically works like the existing DictionaryAdjuster but does
154
+ 1. Add a common password adjuster that basically works like the existing DictionaryAdjuster but does
147
155
  not stop at the first found word. Stopping at the first word make sense if you have a 300,000 word
148
- dictionary of the English language but not so much when you're only talking about the 500 most
156
+ dictionary of the English language but not so much when you're only talking about the 10,000 most
149
157
  common passwords.
150
158
 
159
+ ## Running the tests
160
+
161
+ To run the tests, install the gems with `bundle install`. Then run `rake`.
162
+
151
163
  ## Contributing
152
164
 
153
165
  1. Fork it
154
166
  2. Create your feature branch (`git checkout -b my-new-feature`)
155
- 3. Commit your changes (`git commit -am 'Add some feature'`)
156
- 4. Push to the branch (`git push origin my-new-feature`)
157
- 5. Create new Pull Request
167
+ 3. Make your changes
168
+ 4. Test the changes and make sure existing tests pass
169
+ 5. Commit your changes (`git commit -am 'Add some feature'`)
170
+ 6. Push to the branch (`git push origin my-new-feature`)
171
+ 7. Create new Pull Request
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -4,9 +4,9 @@ module ActiveModel
4
4
  module Validations
5
5
  class PasswordStrengthValidator < ActiveModel::EachValidator
6
6
  def validate_each(object, attribute, value)
7
- ps = ::StrongPassword::StrengthChecker.new(value.to_s)
8
- unless ps.is_strong?(strength_options(options, object))
9
- object.errors.add(attribute, :'password.password_strength', options.merge(:value => value.to_s))
7
+ ps = ::StrongPassword::StrengthChecker.new(**strength_options(options, object))
8
+ unless ps.is_strong?(value.to_s)
9
+ object.errors.add(attribute, :'password.password_strength', **(options.merge(:value => value.to_s)))
10
10
  end
11
11
  end
12
12
 
@@ -40,4 +40,4 @@ module ActiveModel
40
40
  end
41
41
  end
42
42
  end
43
- end
43
+ end
@@ -1,4 +1,4 @@
1
- require 'active_model/validations'
1
+ require 'active_model/validations' if defined?(ActiveModel)
2
2
 
3
3
  require 'strong_password/version'
4
4
  require 'strong_password/nist_bonus_bits'
@@ -12,4 +12,4 @@ require 'active_model/validations/password_strength_validator' if defined?(Activ
12
12
  module StrongPassword
13
13
  end
14
14
 
15
- I18n.load_path << File.dirname(__FILE__) + '/strong_password/locale/en.yml' if defined?(I18n)
15
+ I18n.load_path << File.dirname(__FILE__) + '/strong_password/locale/en.yml' if defined?(I18n)
@@ -967,20 +967,20 @@ module StrongPassword
967
967
  hounds honeydew hooters1 hoes howie hevnm4 hugohugo eighty epson evangeli
968
968
  eeeee1 eyphed ).sort_by(&:length).reverse
969
969
 
970
- attr_reader :base_password
970
+ attr_reader :min_entropy, :min_word_length, :extra_dictionary_words
971
971
 
972
- def initialize(password)
973
- @base_password = password.downcase
972
+ def initialize(min_entropy: 18, min_word_length: 4, extra_dictionary_words: [])
973
+ @min_entropy = min_entropy
974
+ @min_word_length = min_word_length
975
+ @extra_dictionary_words = extra_dictionary_words
974
976
  end
975
977
 
976
- def is_strong?(min_entropy: 18, min_word_length: 4, extra_dictionary_words: [])
977
- adjusted_entropy(entropy_threshhold: min_entropy,
978
- min_word_length: min_word_length,
979
- extra_dictionary_words: extra_dictionary_words) >= min_entropy
978
+ def is_strong?(password)
979
+ adjusted_entropy(password) >= min_entropy
980
980
  end
981
981
 
982
- def is_weak?(min_entropy: 18, min_word_length: 4, extra_dictionary_words: [])
983
- !is_strong?(min_entropy: min_entropy, min_word_length: min_word_length, extra_dictionary_words: extra_dictionary_words)
982
+ def is_weak?(password)
983
+ !is_strong?(password)
984
984
  end
985
985
 
986
986
  # Returns the minimum entropy for the passwords dictionary adjustments.
@@ -988,13 +988,17 @@ module StrongPassword
988
988
  # processing.
989
989
  # Note that we only check for the first matching word up to the threshhold if set.
990
990
  # Subsequent matching words are not deductd.
991
- def adjusted_entropy(min_word_length: 4, extra_dictionary_words: [], entropy_threshhold: -1)
992
- dictionary_words = Regexp.union( ( extra_dictionary_words + COMMON_PASSWORDS ).compact.reject{ |i| i.length < min_word_length } )
991
+ def adjusted_entropy(password)
992
+ base_password = password.downcase
993
993
  min_entropy = EntropyCalculator.calculate(base_password)
994
994
  # Process the passwords, while looking for possible matching words in the dictionary.
995
995
  PasswordVariants.all_variants(base_password).inject( min_entropy ) do |min_entropy, variant|
996
996
  [ min_entropy, EntropyCalculator.calculate( variant.sub( dictionary_words, '*' ) ) ].min
997
997
  end
998
998
  end
999
+
1000
+ def dictionary_words
1001
+ @dictionary_words ||= Regexp.union( ( extra_dictionary_words + COMMON_PASSWORDS ).compact.reject{ |i| i.length < min_word_length } )
1002
+ end
999
1003
  end
1000
- end
1004
+ end
@@ -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)
@@ -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,7 +79,7 @@ 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
85
  passwords = [password.downcase]
@@ -117,37 +91,26 @@ module StrongPassword
117
91
  # Returns all keyboard shifted variants of a given password
118
92
  def self.keyboard_shift_variants(password)
119
93
  password = password.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
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
101
  password = password.downcase
137
- variants = []
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