1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
# frozen-string-literal: true
module Rodauth
Feature.define(:password_complexity, :PasswordComplexity) do
depends :login_password_requirements_base
auth_value_method :password_dictionary_file, nil
auth_value_method :password_dictionary, nil
auth_value_method :password_character_groups, [/[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z\d]/]
auth_value_method :password_min_groups, 3
auth_value_method :password_max_length_for_groups_check, 11
auth_value_method :password_max_repeating_characters, 3
auth_value_method :password_invalid_pattern, Regexp.union([/qwerty/i, /azerty/i, /asdf/i, /zxcv/i] + (1..8).map{|i| /#{i}#{i+1}#{(i+2)%10}/})
translatable_method :password_not_enough_character_groups_message, "does not include uppercase letters, lowercase letters, and numbers"
translatable_method :password_invalid_pattern_message, "includes common character sequence"
translatable_method :password_in_dictionary_message, "is a word in a dictionary"
translatable_method :password_too_many_repeating_characters_message, "contains too many of the same character in a row"
def password_meets_requirements?(password)
super && \
password_has_enough_character_groups?(password) && \
password_has_no_invalid_pattern?(password) && \
password_not_too_many_repeating_characters?(password) && \
password_not_in_dictionary?(password)
end
def post_configure
super
return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
case password_dictionary_file
when false
# nothing
when nil
default_dictionary_file = '/usr/share/dict/words'
# :nocov:
if File.file?(default_dictionary_file)
# :nocov:
words = File.read(default_dictionary_file)
end
else
words = File.read(password_dictionary_file)
end
return unless words
require 'set'
dict = Set.new(words.downcase.split)
self.class.send(:define_method, :password_dictionary){dict}
end
private
def password_has_enough_character_groups?(password)
return true if password.length > password_max_length_for_groups_check
return true if password_character_groups.select{|re| password =~ re}.length >= password_min_groups
set_password_requirement_error_message(:not_enough_character_groups_in_password, password_not_enough_character_groups_message)
false
end
def password_has_no_invalid_pattern?(password)
return true unless password_invalid_pattern
return true if password !~ password_invalid_pattern
set_password_requirement_error_message(:invalid_password_pattern, password_invalid_pattern_message)
false
end
def password_not_too_many_repeating_characters?(password)
return true if password_max_repeating_characters < 2
return true if password !~ /(.)(\1){#{password_max_repeating_characters-1}}/
set_password_requirement_error_message(:too_many_repeating_characters_in_password, password_too_many_repeating_characters_message)
false
end
def password_not_in_dictionary?(password)
return true unless dict = password_dictionary
return true unless password =~ /\A(?:\d*)([A-Za-z!@$+|][A-Za-z!@$+|0134578]+[A-Za-z!@$+|])(?:\d*)\z/
word = $1.downcase.tr('!@$+|0134578', 'iastloleastb')
return true if !dict.include?(word)
set_password_requirement_error_message(:password_in_dictionary, password_in_dictionary_message)
false
end
end
end
|