--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/EricUtilities/EricPasswordChecker.py Fri Sep 27 17:27:11 2024 +0200 @@ -0,0 +1,693 @@ +# -*- 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"]