strong_password 0.0.1

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.
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