Utilities/PasswordChecker.py

changeset 1127
b1802ebe0066
child 1130
3e9f0330f833
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Utilities/PasswordChecker.py	Sun Jun 19 15:19:46 2011 +0200
@@ -0,0 +1,613 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2011 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a checker for password strength
+"""
+
+import re
+
+
+class PasswordChecker(object):
+    """
+    Class implementing a checker for password strength.
+    """
+    Complexity_VeryWeak = 0
+    Complexity_Weak = 1
+    Complexity_Good = 2
+    Complexity_Strong = 3
+    Complexity_VeryStrong = 4
+    
+    Status_Failed = 0
+    Status_Passed = 1
+    Status_Exceeded = 2
+    
+    def __init__(self):
+        """
+        Constructor
+        """
+        self.score = {
+            "count": 0,
+            "adjusted": 0,
+            "beforeRedundancy": 0
+        }
+        
+        # complexity index
+        self.complexity = {
+            "limits": [20, 50, 60, 80, 100],
+            "value": self.Complexity_VeryWeak
+        }
+        
+        # check categories follow
+        
+        # length of the password
+        self.passwordLength = {
+            "count": 0,
+            "minimum": 6,
+            "status": self.Status_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": self.Status_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": self.Status_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": self.Status_Failed,
+            "rating": 0,
+        }
+        
+        # number of uppercase letters, such as A-Z
+        self.uppercaseLetters = {
+            "count": 0,
+            "minimum": 1,
+            "status": self.Status_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": self.Status_Failed,
+            "rating": 0,
+            "factor": 0.0,
+            "bonus": 10,
+            "penalty": -10,
+        }
+        
+        # number of numeric characters
+        self.numerics = {
+            "count": 0,
+            "minimum": 1,
+            "status": self.Status_Failed,
+            "rating": 0,
+            "factor": 0.0,
+            "bonus": 10,
+            "penalty": -10,
+        }
+        
+        # number of symbol characters
+        self.symbols = {
+            "count": 0,
+            "minimum": 1,
+            "status": self.Status_Failed,
+            "rating": 0,
+            "factor": 0.0,
+            "bonus": 10,
+            "penalty": -10,
+        }
+        
+        # number of dedicated symbols in the middle
+        self.middleSymbols = {
+            "count": 0,
+            "minimum": 1,
+            "status": self.Status_Failed,
+            "rating": 0,
+            "factor": 0.0,
+            "bonus": 10,
+            "penalty": -10,
+        }
+        
+        # number of dedicated numbers in the middle
+        self.middleNumerics = {
+            "count": 0,
+            "minimum": 1,
+            "status": self.Status_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": self.Status_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": self.Status_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": self.Status_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": self.Status_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": self.Status_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 (string)
+        @return reversed string (string)
+        """
+        return "".join(reversed(string))
+    
+    def __determineStatus(self, value):
+        """
+        Private method to determine the status.
+        
+        @param value value to check (integer)
+        @return status (Status_Failed, Status_Passed, Status_Exceeded)
+        """
+        if value == 0:
+            return self.Status_Passed
+        elif value > 0:
+            return self.Status_Exceeded
+        else:
+            return self.Status_Failed
+    
+    def __determineBinaryStatus(self, value):
+        """
+        Private method to determine a binary status.
+        
+        @param value value to check (integer)
+        @return status (Status_Failed, Status_Passed)
+        """
+        if value == 0:
+            return self.Status_Passed
+        else:
+            return self.Status_Failed
+    
+    def checkPassword(self, password):
+        """
+        Public method to check a given password.
+        
+        @param password password to be checked (string)
+        @return indication for the password strength (Complexity_VeryWeak,
+            Complexity_Weak, Complexity_Good, Complexity_Strong,
+            Complexity_VeryStrong)
+        """
+        # 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:
+                        if fwd not in patternsMatched:
+                            self.keyboardPatterns["count"] += 1
+                            patternsMatched.append(fwd)
+                    if lowercasedPassword.find(rev) != -1:
+                        if 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"] != self.Status_Failed:
+            # requirement met
+            self.basicRequirements["count"] += 1
+        
+        # uppercase letters
+        self.uppercaseLetters["status"] = self.__determineStatus(
+            self.uppercaseLetters["count"] - self.uppercaseLetters["minimum"])
+        if self.uppercaseLetters["status"] != self.Status_Failed:
+            # requirement met
+            self.basicRequirements["count"] += 1
+        
+        # lowercase letters
+        self.lowercaseLetters["status"] = self.__determineStatus(
+            self.lowercaseLetters["count"] - self.lowercaseLetters["minimum"])
+        if self.lowercaseLetters["status"] != self.Status_Failed:
+            # requirement met
+            self.basicRequirements["count"] += 1
+        
+        # numerics
+        self.numerics["status"] = self.__determineStatus(
+            self.numerics["count"] - self.numerics["minimum"])
+        if self.numerics["status"] != self.Status_Failed:
+            # requirement met
+            self.basicRequirements["count"] += 1
+        
+        # symbols
+        self.symbols["status"] = self.__determineStatus(
+            self.symbols["count"] - self.symbols["minimum"])
+        if self.symbols["status"] != self.Status_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"] != self.Status_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"] != self.Status_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"] != self.Status_Exceeded:
+            # full penalty, because password is not long enough, only for a positive score
+            if self.score["count"] > 0:
+                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"] = index
+                break
+        
+        return self.complexity["value"]
+
+if __name__ == "__main__":
+    while True:
+        try:
+            pwd = input("Enter password: ")
+            print(pwd, PasswordChecker().checkPassword(pwd))
+        except KeyboardInterrupt:
+            break

eric ide

mercurial