strong_password 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1db75b147a9e2bc0833c5de0a098fd8b7584d3b5
4
+ data.tar.gz: 6046844808e8bcf7636cb1865e0218198739a0c1
5
+ SHA512:
6
+ metadata.gz: 0b7544150289fbe379361be135494cc78bdf8d063dffe68f9f9384c82b50dafacb37a3f0ea6c3dcac41ead516595eeea99ecb1b748abb6884a2af8d4a3f6316e
7
+ data.tar.gz: 121dbd08326dccbfb8eacac4378d0db274b0eadd716ae773c009ff3f626811030d59be998470c834cec21fa6244f3cff93e1401b7ef4eb2b0cfd84b45c6aab9e
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ ## v0.0.1
2
+
3
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ version = ENV["RAILS_VERSION"] || "3.2"
6
+
7
+ rails = case version
8
+ when "master"
9
+ {github: "rails/rails"}
10
+ else
11
+ "~> #{version}.0"
12
+ end
13
+
14
+ gem "rails", rails
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Brian McManus
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # StrongPassword
2
+
3
+ StrongPassword provides entropy-based password strength checking for your apps.
4
+
5
+ The idea stemmed from the
6
+ "[Checking Password Strength](http://stackoverflow.com/questions/549/the-definitive-guide-to-forms-based-website-authentication)"
7
+ section on StackOverflow.
8
+
9
+ It is an adaptation of a [PHP algorithm](http://cubicspot.blogspot.com/2011/11/how-to-calculate-password-strength.html)
10
+ developed by Thomas Hruska.
11
+
12
+ ## Installation
13
+
14
+ NOTE: StrongPassword requires the use of Ruby 2.0. Upgrade if you haven't already!
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'strong_password'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install strong_password
27
+
28
+ ## Usage
29
+
30
+ StrongPassword is designed primarily to be used with Rails and ActiveModel validations but can also be
31
+ used standalone if needed.
32
+
33
+ ### With Rails/ActiveModel Validations
34
+
35
+ StrongPassword adds a custom validator for use with ActiveModel validations. Your model does NOT need to be an ActiveRecord
36
+ model, it simply has to be an object that includes `ActiveModel::Validations`. Use it like so:
37
+
38
+ ```ruby
39
+ class User
40
+ include ActiveModel::Validations
41
+
42
+ # Basic usage. Defaults to minimum entropy of 18 and no dictionary checking
43
+ validates :password, password_strength: true
44
+ # Minimum entropy can be specified as min_entropy
45
+ validates :password, password_strength: {min_entropy: 40}
46
+ # Specifies that we want to use dictionary checking
47
+ validates :password, password_strength: {use_dictionary: true}
48
+ # Specifies the minimum size of things that should count as words. Defaults to 4.
49
+ validates :password, password_strength: {use_dictionary: true, min_word_length: 6}
50
+ # Specifies that we want to use dictionary checking and adds 'other', 'common', and 'words' to the dictionary we are checking against.
51
+ validates :password, password_strength: {extra_dictionary_words: ['other', 'common', 'words'], use_dictionary: true}
52
+ # You can also specify a method name to pull the extra words from
53
+ validates :password, password_strength: {extra_dictionary_words: :my_extra_words, use_dictionary: true}
54
+ # Alternative way to request password strength validation on a field
55
+ validates_password_strength :password
56
+
57
+ # Return an array of words to add to the dictionary we check against.
58
+ def my_extra_words
59
+ ['extra', 'words', 'here', 'too']
60
+ end
61
+ end
62
+ ```
63
+
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:
66
+
67
+ ```yml
68
+ en:
69
+ errors:
70
+ messages:
71
+ password:
72
+ password_strength: "%{attribute} is a terrible password, try again!"
73
+ ```
74
+
75
+ ### Standalone
76
+
77
+ StrongPassword can also be used standalone if you need to. There are a few helper methods for determining whether a
78
+ password is strong or not. You can also directly access the entropy calculations if you want.
79
+
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
93
+ ```
94
+
95
+ ## Details
96
+
97
+ So what exactly does StrongPassword do for you? If you're really interested you should [read this article](http://cubicspot.blogspot.com/2011/11/how-to-calculate-password-strength.html)
98
+ from the original author of the algorithm.
99
+
100
+ For the most part StrongPassword stays true to the original but is refactored and tested. I will try to summarize what
101
+ the algorithm does here but reading the code is probably the best way to really get a feel for it.
102
+
103
+ The basic premise is to use the NIST suggested rules for entropy calculation as follows:
104
+
105
+ * The first byte counts as 4 bits.
106
+ * The next 7 bytes count as 2 bits each.
107
+ * The next 12 bytes count as 1.5 bits each.
108
+ * Anything beyond that counts as 1 bit each.
109
+ * Mixed case + non-alphanumeric = up to 6 extra bits.
110
+
111
+ We actually use a modified version of this calculation that degrades the entropy gained for repeat characters by 75% for
112
+ every appearance in the password. The original NIST calculation is still available in `StrongPassword::EntropyCalculator`
113
+ but is generally not used in favor of this stricter (albeit slower) version.
114
+
115
+ So the EntropyCalculator can calculate the entropy of our password for us. What else does the algorithm do?
116
+
117
+ In general the algorithm tries to find the lowest entropy possible for the password by running it against various
118
+ filters. If the lowest entropy we can find for a password is less than the minimum entropy in use by `StrongPassword::StrengthChecker`
119
+ then we consider the password to be weak. The default minimum entropy is 18 bits.
120
+
121
+ The first thing we try is a lowercased version of the password.
122
+
123
+ Next we run the password through a `QwertyAdjuster` that tries to adjust the password's entropy by removing common
124
+ qwerty keyboard layout passwords such as 'qwertyuiop', '1234567890', or '/.,mnbvcxz'. It looks for these in chunks
125
+ between 3 and 6 in length and reduces the entropy of the password accordingly.
126
+
127
+ Finally, if specified, we will run the password through a `DictionaryAdjuster`. This is the slowest part of the
128
+ algorithm but adds significantly to the strength checking. Ideally this should be run against a large dictionary
129
+ of the most commong words in your language (e.g. a 300,000 word English dictionary). Currently I just have this
130
+ hard-coded to run against an embedded list of the 500 most common passwords. This is much faster than running
131
+ against a 300,000 dictionary file and still adds considerably to the strength checking. The password is checked
132
+ against all dictionary words over a minimum word length (default is 4). We also generate variations of
133
+ the password to check for common things like leet speak, and qwerty-shifted passwords. The lowest entropy found
134
+ for any variant is used. Essentially a password of "p4ssw0rd" will be treated as though it were just "password"
135
+ when looking for dictionary words and thus would find "password" and get an extremely low entropy value.
136
+
137
+ You can also supply a list of extra dictionary words to this method via the various `StrongPassword::StrengthChecker`
138
+ methods. Any words supplied will be treated as though they were common dictionary words. This lets you add
139
+ things like your user's first name, last name, and email address as common dictionary words so they will be
140
+ disallowed by the strength checker.
141
+
142
+ ## Todo
143
+
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
147
+ 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
149
+ common passwords.
150
+
151
+ ## Contributing
152
+
153
+ 1. Fork it
154
+ 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
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,43 @@
1
+ require 'strong_password'
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ class PasswordStrengthValidator < ActiveModel::EachValidator
6
+ def validate_each(object, attribute, value)
7
+ ps = ::StrongPassword::StrengthChecker.new(value)
8
+ unless ps.is_strong?(strength_options(options, object))
9
+ object.errors.add(attribute, :'password.password_strength', options.merge(:value => value))
10
+ end
11
+ end
12
+
13
+ def strength_options(options, object)
14
+ strength_options = options.dup
15
+ strength_options[:extra_dictionary_words] = extra_words_for_object(strength_options[:extra_dictionary_words], object)
16
+ strength_options.slice(:min_entropy, :use_dictionary, :min_word_length, :extra_dictionary_words)
17
+ end
18
+
19
+ def extra_words_for_object(extra_words, object)
20
+ return [] unless extra_words.present?
21
+ if object
22
+ if extra_words.respond_to?(:call)
23
+ extra_words = extra_words.call(object)
24
+ elsif extra_words.kind_of? Symbol
25
+ extra_words = object.send(extra_words)
26
+ end
27
+ end
28
+ extra_words || []
29
+ end
30
+ end
31
+
32
+ module HelperMethods
33
+ # class User < ActiveRecord::Base
34
+ # validates_password_strength :password
35
+ # validates_password_strength :password, extra_dictionary_words: :extra_words
36
+ # end
37
+ #
38
+ def validates_password_strength(*attr_names)
39
+ validates_with PasswordStrengthValidator, _merge_attributes(attr_names)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,117 @@
1
+ module StrongPassword
2
+ class DictionaryAdjuster
3
+ COMMON_PASSWORDS = ["123456","password","12345678","1234","pussy","12345","dragon","qwerty",
4
+ "696969","mustang","letmein","baseball","master","michael","football","shadow","monkey","abc123",
5
+ "pass","6969","jordan","harley","ranger","iwantu","jennifer","hunter","2000","test","batman",
6
+ "trustno1","thomas","tigger","robert","access","love","buster","1234567","soccer","hockey","killer",
7
+ "george","sexy","andrew","charlie","superman","asshole","dallas","jessica","panties","pepper",
8
+ "1111","austin","william","daniel","golfer","summer","heather","hammer","yankees","joshua","maggie",
9
+ "biteme","enter","ashley","thunder","cowboy","silver","richard","orange","merlin","michelle",
10
+ "corvette","bigdog","cheese","matthew","121212","patrick","martin","freedom","ginger","blowjob",
11
+ "nicole","sparky","yellow","camaro","secret","dick","falcon","taylor","111111","131313","123123",
12
+ "bitch","hello","scooter","please","","porsche","guitar","chelsea","black","diamond","nascar",
13
+ "jackson","cameron","654321","computer","amanda","wizard","xxxxxxxx","money","phoenix","mickey",
14
+ "bailey","knight","iceman","tigers","purple","andrea","horny","dakota","aaaaaa","player","sunshine",
15
+ "morgan","starwars","boomer","cowboys","edward","charles","girls","booboo","coffee","xxxxxx",
16
+ "bulldog","ncc1701","rabbit","peanut","john","johnny","gandalf","spanky","winter","brandy","compaq",
17
+ "carlos","tennis","james","mike","brandon","fender","anthony","blowme","ferrari","cookie","chicken",
18
+ "maverick","chicago","joseph","diablo","sexsex","hardcore","666666","willie","welcome","chris",
19
+ "panther","yamaha","justin","banana","driver","marine","angels","fishing","david","maddog","hooters",
20
+ "wilson","butthead","dennis","captain","bigdick","chester","smokey","xavier","steven","viking",
21
+ "snoopy","blue","eagles","winner","samantha","house","miller","flower","jack","firebird","butter",
22
+ "united","turtle","steelers","tiffany","zxcvbn","tomcat","golf","bond007","bear","tiger","doctor",
23
+ "gateway","gators","angel","junior","thx1138","porno","badboy","debbie","spider","melissa","booger",
24
+ "1212","flyers","fish","porn","matrix","teens","scooby","jason","walter","cumshot","boston","braves",
25
+ "yankee","lover","barney","victor","tucker","princess","mercedes","5150","doggie","zzzzzz","gunner",
26
+ "horney","bubba","2112","fred","johnson","xxxxx","tits","member","boobs","donald","bigdaddy","bronco",
27
+ "penis","voyager","rangers","birdie","trouble","white","topgun","bigtits","bitches","green","super",
28
+ "qazwsx","magic","lakers","rachel","slayer","scott","2222","asdf","video","london","7777","marlboro",
29
+ "srinivas","internet","action","carter","jasper","monster","teresa","jeremy","11111111","bill","crystal",
30
+ "peter","pussies","cock","beer","rocket","theman","oliver","prince","beach","amateur","7777777","muffin",
31
+ "redsox","star","testing","shannon","murphy","frank","hannah","dave","eagle1","11111","mother","nathan",
32
+ "raiders","steve","forever","angela","viper","ou812","jake","lovers","suckit","gregory","buddy",
33
+ "whatever","young","nicholas","lucky","helpme","jackie","monica","midnight","college","baby","brian",
34
+ "mark","startrek","sierra","leather","232323","4444","beavis","bigcock","happy","sophie","ladies",
35
+ "naughty","giants","booty","blonde","golden","0","fire","sandra","pookie","packers","einstein",
36
+ "dolphins","0","chevy","winston","warrior","sammy","slut","8675309","zxcvbnm","nipples","power",
37
+ "victoria","asdfgh","vagina","toyota","travis","hotdog","paris","rock","xxxx","extreme","redskins",
38
+ "erotic","dirty","ford","freddy","arsenal","access14","wolf","nipple","iloveyou","alex","florida",
39
+ "eric","legend","movie","success","rosebud","jaguar","great","cool","cooper","1313","scorpio",
40
+ "mountain","madison","987654","brazil","lauren","japan","naked","squirt","stars","apple","alexis",
41
+ "aaaa","bonnie","peaches","jasmine","kevin","matt","qwertyui","danielle","beaver","4321","4128",
42
+ "runner","swimming","dolphin","gordon","casper","stupid","shit","saturn","gemini","apples","august",
43
+ "3333","canada","blazer","cumming","hunting","kitty","rainbow","112233","arthur","cream","calvin",
44
+ "shaved","surfer","samson","kelly","paul","mine","king","racing","5555","eagle","hentai","newyork",
45
+ "little","redwings","smith","sticky","cocacola","animal","broncos","private","skippy","marvin",
46
+ "blondes","enjoy","girl","apollo","parker","qwert","time","sydney","women","voodoo","magnum",
47
+ "juice","abgrtyu","777777","dreams","maxwell","music","rush2112","russia","scorpion","rebecca",
48
+ "tester","mistress","phantom","billy","6666","albert"]
49
+
50
+ attr_reader :base_password
51
+
52
+ def initialize(password)
53
+ @base_password = password.dup.downcase
54
+ end
55
+
56
+ def is_strong?(min_entropy: 18, min_word_length: 4, extra_dictionary_words: [])
57
+ adjusted_entropy(entropy_threshhold: min_entropy,
58
+ min_word_length: min_word_length,
59
+ extra_dictionary_words: extra_dictionary_words) >= min_entropy
60
+ end
61
+
62
+ def is_weak?(min_entropy: 18, min_word_length: 4, extra_dictionary_words: [])
63
+ !is_strong?(min_entropy: min_entropy, min_word_length: min_word_length, extra_dictionary_words: extra_dictionary_words)
64
+ end
65
+
66
+ # Returns the minimum entropy for the passwords dictionary adjustments.
67
+ # If a threshhold is specified we will bail early to avoid unnecessary
68
+ # processing.
69
+ # Note that we only check for the first matching word up to the threshhold if set.
70
+ # Subsequent matching words are not deductd.
71
+ def adjusted_entropy(min_word_length: 4, extra_dictionary_words: [], entropy_threshhold: -1)
72
+ dictionary_words = COMMON_PASSWORDS + extra_dictionary_words
73
+ min_entropy = EntropyCalculator.calculate(base_password)
74
+ # Process the passwords, while looking for possible matching words in the dictionary.
75
+ PasswordVariants.all_variants(base_password).each_with_index do |variant, num|
76
+ y = variant.length
77
+ x = -1
78
+ while x < y
79
+ x = x + 1
80
+ if ((variant[x] =~ /\w/) != nil)
81
+ next_non_word = variant.index(/\s/, x)
82
+ x2 = next_non_word ? next_non_word : variant.length + 1
83
+ found = false
84
+ while !found && (x2 - x >= min_word_length)
85
+ word = variant[x, min_word_length]
86
+ word += variant[(x + min_word_length)..x2].reverse.chars.inject('') {|memo, c| "(#{Regexp.quote(c)}#{memo})?"} if (x + min_word_length) <= y
87
+ results = dictionary_words.grep(/\b#{word}\b/)
88
+ if results.empty?
89
+ x = x + 1
90
+ numbits = EntropyCalculator.calculate('*' * x)
91
+ # If we have enough entropy at this length on a fully masked password with
92
+ # duplicates weakened then we can just bail on this variant
93
+ found = true if entropy_threshhold >= 0 && numbits >= entropy_threshhold
94
+ else
95
+ results.each do |match|
96
+ break unless match.present?
97
+ # Substitute a single * for matched portion of word and calculate entropy
98
+ variant = variant.sub(match.strip.sub('-', '\\-'), '*')
99
+ numbits = EntropyCalculator.calculate(variant)
100
+ min_entropy = [min_entropy, numbits].min
101
+ return min_entropy if min_entropy < entropy_threshhold
102
+ end
103
+
104
+ found = true
105
+ end
106
+ end
107
+
108
+ break if found
109
+
110
+ x = x2 - 1
111
+ end
112
+ end
113
+ end
114
+ return min_entropy
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,76 @@
1
+ module StrongPassword
2
+ module EntropyCalculator
3
+ # Calculates NIST entropy for a password.
4
+ def self.calculate(password, repeats_weakened = true)
5
+ if repeats_weakened
6
+ bits_with_repeats_weakened(password)
7
+ else
8
+ bits(password)
9
+ end
10
+ end
11
+
12
+ # The basic NIST entropy calculation is based solely
13
+ # on the length of the password in question.
14
+ def self.bits(password)
15
+ length = password.length
16
+ bits = if length > 20
17
+ 4 + (7 * 2) + (12 * 1.5) + length - 20
18
+ elsif length > 8
19
+ 4 + (7 * 2) + ((length - 8) * 1.5)
20
+ elsif length > 1
21
+ 4 + ((length - 1) * 2)
22
+ else
23
+ (length == 1 ? 4 : 0)
24
+ end
25
+ bits + NistBonusBits.bonus_bits(password)
26
+ end
27
+
28
+ # A modified version of the basic entropy calculation
29
+ # which lowers the amount of entropy gained for each
30
+ # repeated character in the password
31
+ def self.bits_with_repeats_weakened(password)
32
+ resolver = EntropyResolver.new
33
+ bits = password.chars.each.with_index.inject(0) do |result, (char, index)|
34
+ char_value = resolver.entropy_for(char)
35
+ result += bit_value_at_position(index, char_value)
36
+ end
37
+ bits + NistBonusBits.bonus_bits(password)
38
+ end
39
+
40
+ private
41
+
42
+ def self.bit_value_at_position(position, base = 1)
43
+ if position > 19
44
+ return base
45
+ elsif position > 7
46
+ return base * 1.5
47
+ elsif position > 0
48
+ return base * 2
49
+ else
50
+ return 4
51
+ end
52
+ end
53
+
54
+ class EntropyResolver
55
+ BASE_VALUE = 1
56
+ REPEAT_WEAKENING_FACTOR = 0.75
57
+
58
+ attr_reader :char_multiplier
59
+
60
+ def initialize
61
+ @char_multiplier = {}
62
+ end
63
+
64
+ # Returns the current entropy value for a character and weakens the entropy
65
+ # for future calls for the same character.
66
+ def entropy_for(char)
67
+ ordinal_value = char.ord
68
+ char_multiplier[ordinal_value] ||= BASE_VALUE
69
+ char_value = char_multiplier[ordinal_value]
70
+ # Weaken the value of this character for future occurrances
71
+ char_multiplier[ordinal_value] = char_multiplier[ordinal_value] * REPEAT_WEAKENING_FACTOR
72
+ return char_value
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ password:
5
+ password_strength: "%{attribute} is too weak"
@@ -0,0 +1,45 @@
1
+ module StrongPassword
2
+ module NistBonusBits
3
+ @@bonus_bits_for_password = {}
4
+
5
+ # NIST password strength rules allow up to 6 bonus bits for mixed case and non-alphabetic
6
+ def self.bonus_bits(password)
7
+ @@bonus_bits_for_password[password] ||= begin
8
+ calculate_bonus_bits_for(password)
9
+ end
10
+ end
11
+
12
+ # This smells bad as it's only used for testing...
13
+ def self.reset_bonus_cache!
14
+ @@bonus_bits_for_password = {}
15
+ end
16
+
17
+ def self.calculate_bonus_bits_for(password)
18
+ upper = !!(password =~ /[[:upper:]]/)
19
+ lower = !!(password =~ /[[:lower:]]/)
20
+ numeric = !!(password =~ /[[:digit:]]/)
21
+ other = !!(password =~ /[^a-zA-Z0-9 ]/)
22
+ space = !!(password =~ / /)
23
+
24
+ # I had this condensed to nested ternaries but that shit was ugly
25
+ bonus_bits = if upper && lower && other && numeric
26
+ 6
27
+ elsif upper && lower && other && !numeric
28
+ 5
29
+ elsif numeric && other && !upper && !lower
30
+ -2
31
+ elsif numeric && !other && !upper && !lower
32
+ -6
33
+ else
34
+ 0
35
+ end
36
+
37
+ if !space
38
+ bonus_bits = bonus_bits - 2
39
+ elsif password.split(/\s+/).length > 3
40
+ bonus_bits = bonus_bits + 1
41
+ end
42
+ bonus_bits
43
+ end
44
+ end
45
+ end