Class: PasswordIsStrongValidator

Inherits:
ActiveModel::EachValidator
  • Object
show all
Defined in:
app/validators/password_is_strong_validator.rb

Overview

Validates that the password is strong.

Constant Summary collapse

COMMON_PASSWORDS =

Known passwords that should NOT be allowed and should be considered weak.

%w{
  password pass root admin metasploit
  msf 123456 qwerty abc123 letmein monkey link182 demo
  changeme test1234 rapid7
}
SPECIAL_CHARS =

Special characters that are considered to strength passwords and are required once in a strong password.

%q{[email protected]"#$%&'()*+,-./:;<=>?[\\]^_`{|}~ }

Instance Method Summary collapse

Instance Method Details

#contains_username?(username, password) ⇒ true, false (private)

Returns whether username is in password (case-insensitively).

Returns:

  • (true)

    if username is in password.

  • (false)

    unless username is in password.


58
59
60
# File 'app/validators/password_is_strong_validator.rb', line 58

def contains_username?(username, password)
  !!(password =~ /#{username}/i)
end

#is_common_password?(password) ⇒ Boolean (private)

Returns whether password is in COMMON_PASSWORDS or a simple variation of a password in COMMON_PASSWORDS.

Parameters:

  • password (String)

Returns:

  • (Boolean)

66
67
68
69
70
71
72
73
74
75
76
77
# File 'app/validators/password_is_strong_validator.rb', line 66

def is_common_password?(password)
  COMMON_PASSWORDS.each do |pw|
    common_pw = [pw] # pw + "!", pw + "1", pw + "12", pw + "123", pw + "1234"]
    common_pw += mutate_pass(pw)
    common_pw.each do |common_pass|
      if password.downcase =~ /#{common_pass}[\d!]*/
        return true
      end
    end
  end
  false
end

#is_only_repetition?(password) ⇒ Boolean (private)

Returns whether password is only composed of repetitions

Parameters:

  • password (String)

Returns:

  • (Boolean)

119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'app/validators/password_is_strong_validator.rb', line 119

def is_only_repetition?(password)
  # Password repetition (quite basic) -- no "aaaaaa" or "ababab" or "abcabc" or
  # "abcdabcd" (but note that the user can use "aaaaaab" or something).

  if password.scan(/./).uniq.size < 2
    return true
  end

  if (password.size % 2 == 0) and (password.scan(/../).uniq.size < 2)
    return true
  end

  if (password.size % 3 == 0) and (password.scan(/.../).uniq.size < 2)
    return true
  end

  if (password.size % 4 == 0) and (password.scan(/..../).uniq.size < 2)
    return true
  end

  false
end

#is_simple?(password) ⇒ false, true (private)

Returns whether the password is simple.

Returns:

  • (false)

    if password contains a letter, digit and special character.

  • (true)

    otherwise


50
51
52
# File 'app/validators/password_is_strong_validator.rb', line 50

def is_simple?(password)
  not (password =~ /[A-Za-z]/ and password =~ /[0-9]/ and password =~ /[#{Regexp.escape(SPECIAL_CHARS)}]/)
end

#mutate_pass(password) ⇒ String (private)

Returns a leet mutated variant of the original password

Parameters:

  • password (String)

Returns:

  • (String)

    containing the password with leet mutations


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'app/validators/password_is_strong_validator.rb', line 83

def mutate_pass(password)
  mutations = {
      'a' => '@',
      'o' => '0',
      'e' => '3',
      's' => '$',
      't' => '7',
      'l' => '1'
  }

  iterations = mutations.keys.dup
  results = []

  # Find PowerSet of all possible mutation combinations
  iterations = iterations.inject([[]]){|c,y|r=[];c.each{|i|r<<i;r<<i+[y]};r}

  # Iterate through combinations to create each possible mutation
  iterations.each do |iteration|
    next if iteration.flatten.empty?
    first = iteration.shift
    intermediate = password.gsub(/#{first}/i, mutations[first])
    iteration.each do |mutator|
      next unless mutator.kind_of? String
      intermediate.gsub!(/#{mutator}/i, mutations[mutator])
    end
    results << intermediate
  end

  return results
end

#validate_each(record, attribute, value) ⇒ void

This method returns an undefined value.

Validates that the attribute's value on record contains letters, numbers, and at least one special character without containing the record.username, any COMMON_PASSWORDS or repetition.

Parameters:

  • record (#errors, #username, ApplicationRecord)

    ActiveModel or ActiveRecord that supports #username method.

  • attribute (Symbol)

    password attribute name.

  • value (String)

    a password.


24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/validators/password_is_strong_validator.rb', line 24

def validate_each(record, attribute, value)
  return if value.blank?

  if is_simple?(value)
    record.errors.add attribute, 'must contain letters, numbers, and at least one special character'
  end

  if !record.username.blank? && contains_username?(record.username, value)
    record.errors.add attribute, 'must not contain the username'
  end

  if is_common_password?(value)
    record.errors.add attribute, 'must not be a common password'
  end

  if is_only_repetition?(value)
    record.errors.add attribute, 'must not be a predictable sequence of characters'
  end
end