Class: PasswordStrength::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/password_strength/base.rb

Direct Known Subclasses

Validators::Windows2008

Constant Summary collapse

MULTIPLE_NUMBERS_RE =
/\d.*?\d.*?\d/
MULTIPLE_SYMBOLS_RE =
/[!@#\$%^&*?_~-].*?[!@#\$%^&*?_~-]/
SYMBOL_RE =
/[!@#\$%^&*?_~-]/
UPPERCASE_LOWERCASE_RE =
/([a-z].*[A-Z])|([A-Z].*[a-z])/
INVALID =
:invalid
WEAK =
:weak
STRONG =
:strong
GOOD =
:good

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(username, password, options = {}) ⇒ Base

Returns a new instance of Base.



63
64
65
66
67
68
69
# File 'lib/password_strength/base.rb', line 63

def initialize(username, password, options = {})
  @username = username.to_s
  @password = password.to_s
  @score = 0
  @exclude = options[:exclude]
  @record = options[:record]
end

Instance Attribute Details

#excludeObject

Set what characters cannot be present on password. Can be a regular expression or array.

strength = PasswordStrength.test("john", "password with whitespaces", :exclude => [" ", "asdf"])
strength = PasswordStrength.test("john", "password with whitespaces", :exclude => /\s/)

Then you can check the test result:

strength.valid?(:weak)
#=> false

strength.status
#=> :invalid


42
43
44
# File 'lib/password_strength/base.rb', line 42

def exclude
  @exclude
end

#passwordObject

The password that will be tested.



16
17
18
# File 'lib/password_strength/base.rb', line 16

def password
  @password
end

#recordObject (readonly)

The ActiveRecord instance. It only makes sense if you’re creating a custom ActiveRecord validator.



26
27
28
# File 'lib/password_strength/base.rb', line 26

def record
  @record
end

#scoreObject (readonly)

The score for the latest test. Will be nil if the password has not been tested.



19
20
21
# File 'lib/password_strength/base.rb', line 19

def score
  @score
end

#statusObject (readonly)

The current test status. Can be :weak, :good, :strong or :invalid.



22
23
24
# File 'lib/password_strength/base.rb', line 22

def status
  @status
end

#usernameObject

Hold the username that will be matched against password.



13
14
15
# File 'lib/password_strength/base.rb', line 13

def username
  @username
end

Class Method Details

.common_wordsObject

Return an array of strings that represents common passwords. The default list is taken from several online sources (just Google for ‘most common passwords’).

Notable sources:

The current list has 3.6KB and its load into memory just once.



54
55
56
57
58
59
60
61
# File 'lib/password_strength/base.rb', line 54

def self.common_words
  @common_words ||= begin
    file = File.open(File.expand_path("../../../support/common.txt", __FILE__))
    words = file.each_line.to_a.map(&:chomp)
    file.close
    words
  end
end

Instance Method Details

#common_word?Boolean

:nodoc:

Returns:

  • (Boolean)


217
218
219
# File 'lib/password_strength/base.rb', line 217

def common_word? # :nodoc:
  self.class.common_words.include?(password.downcase)
end

#contain_invalid_matches?Boolean

:nodoc:

Returns:

  • (Boolean)


221
222
223
224
225
226
# File 'lib/password_strength/base.rb', line 221

def contain_invalid_matches? # :nodoc:
  return false unless exclude
  regex = exclude
  regex = /#{exclude.collect {|i| Regexp.escape(i)}.join("|")}/ if exclude.kind_of?(Array)
  password.to_s =~ regex
end

#contain_invalid_repetion?Boolean

Returns:

  • (Boolean)


228
229
230
231
232
233
# File 'lib/password_strength/base.rb', line 228

def contain_invalid_repetion?
  char = password.to_s.chars.first
  return unless char
  regex = /^#{Regexp.escape(char)}+$/i
  password.to_s =~ regex
end

#good!Object

Mark password as good.



110
111
112
# File 'lib/password_strength/base.rb', line 110

def good!
  @status = GOOD
end

#good?Boolean

Check if the password has been detected as good.

Returns:

  • (Boolean)


105
106
107
# File 'lib/password_strength/base.rb', line 105

def good?
  status == GOOD
end

#invalid!Object

Mark password as invalid.



120
121
122
# File 'lib/password_strength/base.rb', line 120

def invalid!
  @status = INVALID
end

#invalid?Boolean

Check if password has invalid characters based on PasswordStrength::Base#exclude.

Returns:

  • (Boolean)


115
116
117
# File 'lib/password_strength/base.rb', line 115

def invalid?
  status == INVALID
end

#repetitions(text, size) ⇒ Object

:nodoc:



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/password_strength/base.rb', line 235

def repetitions(text, size) # :nodoc:
  count = 0
  matches = []

  0.upto(text.size - 1) do |i|
    substring = text[i, size]

    next if matches.include?(substring) || substring.size < size

    matches << substring
    occurrences = text.scan(/#{Regexp.escape(substring)}/).length
    count += 1 if occurrences > 1
  end

  count
end

#score_for(name) ⇒ Object

Return the score for the specified rule. Available rules:

  • :password_size

  • :numbers

  • :symbols

  • :uppercase_lowercase

  • :numbers_chars

  • :numbers_symbols

  • :symbols_chars

  • :only_chars

  • :only_numbers

  • :username

  • :sequences



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/password_strength/base.rb', line 138

def score_for(name)
  score = 0

  case name
  when :password_size then
    if password.size < 6
      score = -100
    else
      score = password.size * 4
    end
  when :numbers then
    score = 5 if password =~ MULTIPLE_NUMBERS_RE
  when :symbols then
    score = 5 if password =~ MULTIPLE_SYMBOLS_RE
  when :uppercase_lowercase then
    score = 10 if password =~ UPPERCASE_LOWERCASE_RE
  when :numbers_chars then
    score = 15 if password =~ /[a-z]/i && password =~ /[0-9]/
  when :numbers_symbols then
    score = 15 if password =~ /[0-9]/ && password =~ SYMBOL_RE
  when :symbols_chars then
    score = 15 if password =~ /[a-z]/i && password =~ SYMBOL_RE
  when :only_chars then
    score = -15 if password =~ /^[a-z]+$/i
  when :only_numbers then
    score = -15 if password =~ /^\d+$/
  when :username then
    if password == username
      score = -100
    else
      score = -15 if password =~ /#{Regexp.escape(username)}/
    end
  when :sequences then
    score = -15 * sequences(password)
    score += -15 * sequences(password.to_s.reverse)
  when :repetitions then
    score += -(repetitions(password, 2) * 4)
    score += -(repetitions(password, 3) * 3)
    score += -(repetitions(password, 4) * 2)
  end

  score
end

#sequences(text) ⇒ Object

:nodoc:



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/password_strength/base.rb', line 252

def sequences(text) # :nodoc:
  matches = 0
  sequence_size = 0
  bytes = []

  text.to_s.each_byte do |byte|
    previous_byte = bytes.last
    bytes << byte

    if previous_byte && ((byte == previous_byte + 1) || (previous_byte == byte))
      sequence_size += 1
    else
      sequence_size = 0
    end

    matches += 1 if sequence_size == 2
  end

  matches
end

#strong!Object

Mark password as strong.



90
91
92
# File 'lib/password_strength/base.rb', line 90

def strong!
  @status = STRONG
end

#strong?Boolean

Check if the password has been detected as strong.

Returns:

  • (Boolean)


85
86
87
# File 'lib/password_strength/base.rb', line 85

def strong?
  status == STRONG
end

#testObject

Run all tests on password and return the final score.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/password_strength/base.rb', line 183

def test
  @score = 0

  if contain_invalid_matches?
    invalid!
  elsif common_word?
    invalid!
  elsif contain_invalid_repetion?
    invalid!
  else
    @score += score_for(:password_size)
    @score += score_for(:numbers)
    @score += score_for(:symbols)
    @score += score_for(:uppercase_lowercase)
    @score += score_for(:numbers_chars)
    @score += score_for(:numbers_symbols)
    @score += score_for(:symbols_chars)
    @score += score_for(:only_chars)
    @score += score_for(:only_numbers)
    @score += score_for(:username)
    @score += score_for(:sequences)
    @score += score_for(:repetitions)

    @score = 0 if score < 0
    @score = 100 if score > 100

    weak!   if score < 35
    good!   if score >= 35 && score < 70
    strong! if score >= 70
  end

  score
end

#valid?(level = GOOD) ⇒ Boolean

Check if the password has the specified score. Level can be :weak, :good or :strong.

Returns:

  • (Boolean)


73
74
75
76
77
78
79
80
81
82
# File 'lib/password_strength/base.rb', line 73

def valid?(level = GOOD)
  case level
  when STRONG then
    strong?
  when GOOD then
    good? || strong?
  else
    !invalid?
  end
end

#weak!Object

Mark password as weak.



100
101
102
# File 'lib/password_strength/base.rb', line 100

def weak!
  @status = WEAK
end

#weak?Boolean

Check if the password has been detected as weak.

Returns:

  • (Boolean)


95
96
97
# File 'lib/password_strength/base.rb', line 95

def weak?
  status == WEAK
end