Sat, 06 Jan 2024 15:21:02 +0100
Changed some state/mode definitiuons to an enum.Enum class and corrected some code style and formatting issues.
# -*- coding: utf-8 -*- # Copyright (c) 2011 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a checker for password strength. """ import enum import re class PasswordStrength(enum.IntEnum): """ Class defining the password strength. """ VeryWeak = 0 Weak = 1 Good = 2 Strong = 3 VeryStrong = 4 class PasswordCheckStatus(enum.Enum): """ Class defining the status of a password check. """ Failed = 0 Passed = 1 Exceeded = 2 class PasswordChecker: """ Class implementing a checker for password strength. """ def __init__(self): """ Constructor """ self.score = {"count": 0, "adjusted": 0, "beforeRedundancy": 0} # complexity index self.complexity = { "limits": [20, 50, 60, 80, 100], "value": PasswordStrength.VeryWeak, } # check categories follow # length of the password self.passwordLength = { "count": 0, "minimum": 6, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.5, # per character bonus "bonus": 10, # minimum reached? Get a bonus. "penalty": -20, # if we stay under minimum, we get punished } # recommended password length self.recommendedPasswordLength = { "count": 0, "minimum": 8, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 1.2, "bonus": 10, "penalty": -10, } # Basic requirements are: # 1) Password Length # 2) Uppercase letter use # 3) Lowercase letter use # 4) Numeric character use # 5) Symbol use self.basicRequirements = { "count": 0, "minimum": 3, # have to be matched to get the bonus "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 1.0, "bonus": 10, "penalty": -10, } # how much redundancy is permitted, if the password is # long enough. we will skip the redudancy penalty if this # number is not exceeded (meaning redundancy < this number) self.redundancy = { "value": 1, # 1 means, not double characters, # default to start "permitted": 2.0, # 2 means, in average every character # can occur twice "status": PasswordCheckStatus.Failed, "rating": 0, } # number of uppercase letters, such as A-Z self.uppercaseLetters = { "count": 0, "minimum": 1, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 10, "penalty": -10, } # number of lowercase letters, such as a-z self.lowercaseLetters = { "count": 0, "minimum": 1, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 10, "penalty": -10, } # number of numeric characters self.numerics = { "count": 0, "minimum": 1, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 10, "penalty": -10, } # number of symbol characters self.symbols = { "count": 0, "minimum": 1, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 10, "penalty": -10, } # number of dedicated symbols in the middle self.middleSymbols = { "count": 0, "minimum": 1, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 10, "penalty": -10, } # number of dedicated numbers in the middle self.middleNumerics = { "count": 0, "minimum": 1, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 10, "penalty": -10, } # how many sequential characters should be checked # such as "abc" or "MNO" to be not part of the password self.sequentialLetters = { "data": "abcdefghijklmnopqrstuvwxyz", "length": 3, "count": 0, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": -1.0, "bonus": 0, "penalty": -10, } # how many sequential characters should be checked # such as "123" to be not part of the password self.sequentialNumerics = { "data": "0123456789", "length": 3, "count": 0, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": -1.0, "bonus": 0, "penalty": -10, } # keyboard patterns to check, typical sequences from your # keyboard self.keyboardPatterns = { # German and English keyboard text "data": [ "qwertzuiop", "asdfghjkl", "yxcvbnm", '!"§$%&/()=', # de "1234567890", # de numbers "qaywsxedcrfvtgbzhnujmik,ol.pö-üä+#", # de up-down "qwertyuiop", "asdfghjkl", "zyxcvbnm", "!@#$%^&*()_", # en "1234567890", # en numbers "qazwsxedcrfvtgbyhnujmik,ol.p;/[']\\", # en up-down ], "length": 4, # how long is the pattern to check and blame for? "count": 0, # how many of these pattern can be found "status": PasswordCheckStatus.Failed, "rating": 0, "factor": -1.0, # each occurrence is punished with that factor "bonus": 0, "penalty": -10, } # check for repeated sequences, like in catcat self.repeatedSequences = { "length": 3, "count": 0, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 0, "penalty": -10, } # check for repeated mirrored sequences, like in tactac self.mirroredSequences = { "length": 3, "count": 0, "status": PasswordCheckStatus.Failed, "rating": 0, "factor": 0.0, "bonus": 0, "penalty": -10, } self.uppercaseRe = re.compile("[A-Z]") self.lowercaseRe = re.compile("[a-z]") self.numberRe = re.compile("[0-9]") self.symbolRe = re.compile("[^a-zA-Z0-9]") def __strReverse(self, string): """ Private method to reverse a string. @param string string to be reversed @type str @return reversed string @rtype str """ return "".join(reversed(string)) def __determineStatus(self, value): """ Private method to determine the status. @param value value to check @type int @return status @rtype PasswordCheckStatus """ if value == 0: return PasswordCheckStatus.Passed elif value > 0: return PasswordCheckStatus.Exceeded else: return PasswordCheckStatus.Failed def __determineBinaryStatus(self, value): """ Private method to determine a binary status. @param value value to check @type int @return status @rtype PasswordCheckStatus """ if value == 0: return PasswordCheckStatus.Passed else: return PasswordCheckStatus.Failed def checkPassword(self, password): """ Public method to check a given password. @param password password to be checked @type str @return indication for the password strength @rtype PasswordStrength """ # how long is the password? self.passwordLength["count"] = len(password) self.recommendedPasswordLength["count"] = len(password) # Loop through password to check for Symbol, Numeric, Lowercase # and Uppercase pattern matches for index in range(len(password)): if self.uppercaseRe.match(password[index]): self.uppercaseLetters["count"] += 1 elif self.lowercaseRe.match(password[index]): self.lowercaseLetters["count"] += 1 elif self.numberRe.match(password[index]): if index > 0 and index < len(password) - 1: self.middleNumerics["count"] += 1 self.numerics["count"] += 1 elif self.symbolRe.match(password[index]): if index > 0 and index < len(password) - 1: self.middleSymbols["count"] += 1 self.symbols["count"] += 1 # check the variance of symbols or better the redundancy # makes only sense for at least two characters if len(password) > 1: uniqueCharacters = [] for index1 in range(len(password)): found = False for index2 in range(index1 + 1, len(password)): if password[index1] == password[index2]: found = True break if not found: uniqueCharacters.append(password[index1]) # calculate a redundancy number self.redundancy["value"] = len(password) / len(uniqueCharacters) # Check for sequential alpha string patterns (forward and reverse) # but only, if the string has already a length to check for, does # not make sense to check the password "ab" for the sequential data # "abc" lowercasedPassword = password.lower() if self.passwordLength["count"] >= self.sequentialLetters["length"]: for index in range( len(self.sequentialLetters["data"]) - self.sequentialLetters["length"] + 1 ): fwd = self.sequentialLetters["data"][ index : index + self.sequentialLetters["length"] ] rev = self.__strReverse(fwd) if lowercasedPassword.find(fwd) != -1: self.sequentialLetters["count"] += 1 if lowercasedPassword.find(rev) != -1: self.sequentialLetters["count"] += 1 # Check for sequential numeric string patterns (forward and reverse) if self.passwordLength["count"] >= self.sequentialNumerics["length"]: for index in range( len(self.sequentialNumerics["data"]) - self.sequentialNumerics["length"] + 1 ): fwd = self.sequentialNumerics["data"][ index : index + self.sequentialNumerics["length"] ] rev = self.__strReverse(fwd) if lowercasedPassword.find(fwd) != -1: self.sequentialNumerics["count"] += 1 if lowercasedPassword.find(rev) != -1: self.sequentialNumerics["count"] += 1 # Check common keyboard patterns patternsMatched = [] if self.passwordLength["count"] >= self.keyboardPatterns["length"]: for pattern in self.keyboardPatterns["data"]: for index in range(len(pattern) - self.keyboardPatterns["length"] + 1): fwd = pattern[index : index + self.keyboardPatterns["length"]] rev = self.__strReverse(fwd) if ( lowercasedPassword.find(fwd) != -1 and fwd not in patternsMatched ): self.keyboardPatterns["count"] += 1 patternsMatched.append(fwd) if ( lowercasedPassword.find(rev) != -1 and fwd not in patternsMatched ): self.keyboardPatterns["count"] += 1 patternsMatched.append(rev) # Try to find repeated sequences of characters. if self.passwordLength["count"] >= self.repeatedSequences["length"]: for index in range( len(lowercasedPassword) - self.repeatedSequences["length"] + 1 ): fwd = lowercasedPassword[ index : index + self.repeatedSequences["length"] ] if ( lowercasedPassword.find( fwd, index + self.repeatedSequences["length"] ) != -1 ): self.repeatedSequences["count"] += 1 # Try to find mirrored sequences of characters. if self.passwordLength["count"] >= self.mirroredSequences["length"]: for index in range( len(lowercasedPassword) - self.mirroredSequences["length"] + 1 ): fwd = lowercasedPassword[ index : index + self.mirroredSequences["length"] ] rev = self.__strReverse(fwd) if ( lowercasedPassword.find( fwd, index + self.mirroredSequences["length"] ) != -1 ): self.mirroredSequences["count"] += 1 # Initial score based on length self.score["count"] = ( self.passwordLength["count"] * self.passwordLength["factor"] ) # passwordLength # credit additional length or punish "under" length if self.passwordLength["count"] >= self.passwordLength["minimum"]: # credit additional characters over minimum self.passwordLength["rating"] = ( self.passwordLength["bonus"] + (self.passwordLength["count"] - self.passwordLength["minimum"]) * self.passwordLength["factor"] ) else: self.passwordLength["rating"] = self.passwordLength["penalty"] self.score["count"] += self.passwordLength["rating"] # recommendedPasswordLength # Credit reaching the recommended password length or put a # penalty on it if self.passwordLength["count"] >= self.recommendedPasswordLength["minimum"]: self.recommendedPasswordLength["rating"] = ( self.recommendedPasswordLength["bonus"] + ( self.passwordLength["count"] - self.recommendedPasswordLength["minimum"] ) * self.recommendedPasswordLength["factor"] ) else: self.recommendedPasswordLength["rating"] = self.recommendedPasswordLength[ "penalty" ] self.score["count"] += self.recommendedPasswordLength["rating"] # lowercaseLetters # Honor or punish the lowercase letter use if self.lowercaseLetters["count"] > 0: self.lowercaseLetters["rating"] = ( self.lowercaseLetters["bonus"] + self.lowercaseLetters["count"] * self.lowercaseLetters["factor"] ) else: self.lowercaseLetters["rating"] = self.lowercaseLetters["penalty"] self.score["count"] += self.lowercaseLetters["rating"] # uppercaseLetters # Honor or punish the lowercase letter use if self.uppercaseLetters["count"] > 0: self.uppercaseLetters["rating"] = ( self.uppercaseLetters["bonus"] + self.uppercaseLetters["count"] * self.uppercaseLetters["factor"] ) else: self.uppercaseLetters["rating"] = self.uppercaseLetters["penalty"] self.score["count"] += self.uppercaseLetters["rating"] # numerics # Honor or punish the numerics use if self.numerics["count"] > 0: self.numerics["rating"] = ( self.numerics["bonus"] + self.numerics["count"] * self.numerics["factor"] ) else: self.numerics["rating"] = self.numerics["penalty"] self.score["count"] += self.numerics["rating"] # symbols # Honor or punish the symbols use if self.symbols["count"] > 0: self.symbols["rating"] = ( self.symbols["bonus"] + self.symbols["count"] * self.symbols["factor"] ) else: self.symbols["rating"] = self.symbols["penalty"] self.score["count"] += self.symbols["rating"] # middleSymbols # Honor or punish the middle symbols use if self.middleSymbols["count"] > 0: self.middleSymbols["rating"] = ( self.middleSymbols["bonus"] + self.middleSymbols["count"] * self.middleSymbols["factor"] ) else: self.middleSymbols["rating"] = self.middleSymbols["penalty"] self.score["count"] += self.middleSymbols["rating"] # middleNumerics # Honor or punish the middle numerics use if self.middleNumerics["count"] > 0: self.middleNumerics["rating"] = ( self.middleNumerics["bonus"] + self.middleNumerics["count"] * self.middleNumerics["factor"] ) else: self.middleNumerics["rating"] = self.middleNumerics["penalty"] self.score["count"] += self.middleNumerics["rating"] # sequentialLetters # Honor or punish the sequential letter use if self.sequentialLetters["count"] == 0: self.sequentialLetters["rating"] = ( self.sequentialLetters["bonus"] + self.sequentialLetters["count"] * self.sequentialLetters["factor"] ) else: self.sequentialLetters["rating"] = self.sequentialLetters["penalty"] self.score["count"] += self.sequentialLetters["rating"] # sequentialNumerics # Honor or punish the sequential numerics use if self.sequentialNumerics["count"] == 0: self.sequentialNumerics["rating"] = ( self.sequentialNumerics["bonus"] + self.sequentialNumerics["count"] * self.sequentialNumerics["factor"] ) else: self.sequentialNumerics["rating"] = self.sequentialNumerics["penalty"] self.score["count"] += self.sequentialNumerics["rating"] # keyboardPatterns # Honor or punish the keyboard patterns use if self.keyboardPatterns["count"] == 0: self.keyboardPatterns["rating"] = ( self.keyboardPatterns["bonus"] + self.keyboardPatterns["count"] * self.keyboardPatterns["factor"] ) else: self.keyboardPatterns["rating"] = self.keyboardPatterns["penalty"] self.score["count"] += self.keyboardPatterns["rating"] # Count our basicRequirements and set the status self.basicRequirements["count"] = 0 # password length self.passwordLength["status"] = self.__determineStatus( self.passwordLength["count"] - self.passwordLength["minimum"] ) if self.passwordLength["status"] != PasswordCheckStatus.Failed: # requirement met self.basicRequirements["count"] += 1 # uppercase letters self.uppercaseLetters["status"] = self.__determineStatus( self.uppercaseLetters["count"] - self.uppercaseLetters["minimum"] ) if self.uppercaseLetters["status"] != PasswordCheckStatus.Failed: # requirement met self.basicRequirements["count"] += 1 # lowercase letters self.lowercaseLetters["status"] = self.__determineStatus( self.lowercaseLetters["count"] - self.lowercaseLetters["minimum"] ) if self.lowercaseLetters["status"] != PasswordCheckStatus.Failed: # requirement met self.basicRequirements["count"] += 1 # numerics self.numerics["status"] = self.__determineStatus( self.numerics["count"] - self.numerics["minimum"] ) if self.numerics["status"] != PasswordCheckStatus.Failed: # requirement met self.basicRequirements["count"] += 1 # symbols self.symbols["status"] = self.__determineStatus( self.symbols["count"] - self.symbols["minimum"] ) if self.symbols["status"] != PasswordCheckStatus.Failed: # requirement met self.basicRequirements["count"] += 1 # judge the requirement status self.basicRequirements["status"] = self.__determineStatus( self.basicRequirements["count"] - self.basicRequirements["minimum"] ) if self.basicRequirements["status"] != PasswordCheckStatus.Failed: self.basicRequirements["rating"] = ( self.basicRequirements["bonus"] + self.basicRequirements["factor"] * self.basicRequirements["count"] ) else: self.basicRequirements["rating"] = self.basicRequirements["penalty"] self.score["count"] += self.basicRequirements["rating"] # beyond basic requirements self.recommendedPasswordLength["status"] = self.__determineStatus( self.recommendedPasswordLength["count"] - self.recommendedPasswordLength["minimum"] ) self.middleNumerics["status"] = self.__determineStatus( self.middleNumerics["count"] - self.middleNumerics["minimum"] ) self.middleSymbols["status"] = self.__determineStatus( self.middleSymbols["count"] - self.middleSymbols["minimum"] ) self.sequentialLetters["status"] = self.__determineBinaryStatus( self.sequentialLetters["count"] ) self.sequentialNumerics["status"] = self.__determineBinaryStatus( self.sequentialNumerics["count"] ) self.keyboardPatterns["status"] = self.__determineBinaryStatus( self.keyboardPatterns["count"] ) self.repeatedSequences["status"] = self.__determineBinaryStatus( self.repeatedSequences["count"] ) self.mirroredSequences["status"] = self.__determineBinaryStatus( self.mirroredSequences["count"] ) # we apply them only, if the length is not awesome if self.recommendedPasswordLength["status"] != PasswordCheckStatus.Exceeded: # repeatedSequences # Honor or punish the use of repeated sequences if self.repeatedSequences["count"] == 0: self.repeatedSequences["rating"] = self.repeatedSequences["bonus"] else: self.repeatedSequences["rating"] = ( self.repeatedSequences["penalty"] + self.repeatedSequences["count"] * self.repeatedSequences["factor"] ) # mirroredSequences # Honor or punish the use of mirrored sequences if self.mirroredSequences["count"] == 0: self.mirroredSequences["rating"] = self.mirroredSequences["bonus"] else: self.mirroredSequences["rating"] = ( self.mirroredSequences["penalty"] + self.mirroredSequences["count"] * self.mirroredSequences["factor"] ) # save value before redundancy self.score["beforeRedundancy"] = self.score["count"] # apply the redundancy # is the password length requirement fulfilled? if ( self.recommendedPasswordLength["status"] != PasswordCheckStatus.Exceeded and self.score["count"] > 0 ): # full penalty, because password is not long enough, only for # a positive score self.score["count"] *= 1.0 / self.redundancy["value"] # level it out if self.score["count"] > 100: self.score["adjusted"] = 100 elif self.score["count"] < 0: self.score["adjusted"] = 0 else: self.score["adjusted"] = self.score["count"] # judge it for index in range(len(self.complexity["limits"])): if self.score["adjusted"] <= self.complexity["limits"][index]: self.complexity["value"] = PasswordStrength(index) break return self.complexity["value"]