def calculate_entropy(password: str, method: str = "normal", character_pool: CharacterPool = None): """ Calculate the entropy of a password according to the formula: log base 2 (number of possible passwords) :param password: the password :type: str :param method: method to calculate the pool of characters :type: str ("strict", "normal" or "lenient") :param character_pool: pool of characters to use :type: CharacterPool :return: Entropy of password :type: float """ # set default char pool if user doesn't pass one if character_pool is None: pool = CharacterPool() # else set user char pool, should be init'd or have viable class methods else: pool = character_pool # all chars must be in password char pool if not all(i in pool.all for i in password): raise UnacceptableCharacters( f"You can only use characters from the character pool, " f"which are: {pool.all}") else: number_of_possible_passwords = calculate_number_of_possible_passwords( password, method, pool) return math.log(number_of_possible_passwords, 2)
def test_normal_evaluation_triples(): pool = CharacterPool() # triples password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.uppercase | pool.symbols ) assert normal_pool_of_unique_characters(password) == 26 + 26 + 32 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.uppercase | pool.numbers ) assert normal_pool_of_unique_characters(password) == len(pool.alphanumeric) assert normal_pool_of_unique_characters(password) == 26 + 26 + 10 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.symbols | pool.numbers ) assert normal_pool_of_unique_characters(password) == 26 + 32 + 10 # password = "******" assert normal_pool_of_unique_characters(password) == len( pool.uppercase | pool.symbols | pool.numbers ) assert normal_pool_of_unique_characters(password) == 26 + 32 + 10
def random_pool(): pool = CharacterPool( lowercase="123xyz", uppercase="ABC!", symbols="<@>", whitespace="to", numbers="cvbnm,", other="65;", ) yield pool
def test_normal_evaluation_quadruples(): pool = CharacterPool() # quadruple password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.uppercase | pool.symbols | pool.numbers ) assert normal_pool_of_unique_characters(password) == 94
def __init__(self, password: str, character_pool: CharacterPool = None): # set character pool if character_pool is None: self.pool = CharacterPool() else: self.pool = character_pool assert all( i in self.pool.all for i in password ), "A password can only use characters from the character_pool provided" # set password self.password = password # set the number of lowercase in the password self.lowercase = len([ character for character in password if character in self.pool.lowercase ]) # set the uppercase of lowercase in the password self.uppercase = len([ character for character in password if character in self.pool.uppercase ]) # set the symbols of lowercase in the password self.symbols = len([ character for character in password if character in self.pool.symbols ]) # set the numbers of lowercase in the password self.numbers = len([ character for character in password if character in self.pool.numbers ]) # set the numbers of whitespace in the password self.whitespace = len([ character for character in password if character in self.pool.whitespace ]) # set the other of lowercase in the password self.other = len([ character for character in password if character in self.pool.other ]) # set the length of the password self.length = len(password) # set entropy self.entropy = calculate_entropy(password, character_pool=self.pool)
def test_character_pool(): pool = CharacterPool() assert pool.lowercase == set("abcdefghijklmnopqrstuvwxyz") assert pool.uppercase == set("ABCDEFGHIJKLMNOPQRSTUVWXYZ") assert pool.numbers == set("0123456789") assert pool.symbols == set(r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""") assert pool.whitespace == set(" ") assert pool.other == set() assert pool.letters == pool.lowercase | pool.uppercase assert pool.alphanumeric == pool.letters | pool.numbers assert pool.all == pool.alphanumeric | pool.symbols | pool.whitespace | pool.other
def test_character_pool_init_with_kwargs(): pool = CharacterPool( lowercase="abc", uppercase="XYZ", numbers="123", symbols="()", whitespace=" ", other="é", ) assert pool.lowercase == set("abc") assert pool.uppercase == set("XYZ") assert pool.numbers == set("123") assert pool.symbols == set("()") assert pool.whitespace == set(" ") assert pool.other == set("é") assert pool.letters == pool.lowercase | pool.uppercase assert pool.alphanumeric == pool.letters | pool.numbers assert pool.all == pool.alphanumeric | pool.symbols | pool.whitespace | pool.other
def test_strict_evaluation(): pool = CharacterPool() password = "******" assert strict_pool_of_unique_characters(password) == len(password) assert strict_pool_of_unique_characters(password) == 10 password = "******" assert strict_pool_of_unique_characters(password) == len(set(password)) assert strict_pool_of_unique_characters(password) == 4 password = "******" assert strict_pool_of_unique_characters(password) == 6 password = "******" assert strict_pool_of_unique_characters(password) == 10 password = "******" assert strict_pool_of_unique_characters(password) == 12 password = "******" assert strict_pool_of_unique_characters(password) == 11 password = pool.lowercase assert strict_pool_of_unique_characters(password) == len(pool.lowercase) assert strict_pool_of_unique_characters(password) == 26 password = pool.uppercase assert strict_pool_of_unique_characters(password) == len(pool.uppercase) assert strict_pool_of_unique_characters(password) == 26 password = pool.numbers assert strict_pool_of_unique_characters(password) == len(pool.numbers) assert strict_pool_of_unique_characters(password) == 10 password = pool.whitespace assert strict_pool_of_unique_characters(password) == len(pool.whitespace) assert strict_pool_of_unique_characters(password) == 1 password = pool.symbols assert strict_pool_of_unique_characters(password) == len(pool.symbols) assert strict_pool_of_unique_characters(password) == 32
def calculate_number_of_possible_passwords( password: str, method: str = "normal", character_pool: CharacterPool = None): """ Calculate the number of possible passwords according to the formula: size of pool of characters (int) ^ number of characters in password (int) :param password: the password :type: str :param method: method to calculate the pool of characters :type: str ("strict", "normal" or "lenient") :param character_pool: pool of characters to use :type: CharacterPool :return: Number Of Possible Passwords :type: int """ # set default char pool if user doesn't pass one if character_pool is None: pool = CharacterPool() # else set user char pool, should be init'd or have viable class methods else: pool = character_pool # user can pick between strict, normal and lenient if method == "strict": pool_of_characters = strict_pool_of_unique_characters(password) elif method == "normal": pool_of_characters = normal_pool_of_unique_characters( password, character_pool=pool) elif method == "lenient": pool_of_characters = lenient_pool_of_unique_characters( character_pool=pool) else: raise ValueError( 'method must be either "strict", "normal" or "lenient"') # return pool of chars ^ len password return pool_of_characters**len(password)
def test_normal_evaluation_doubles(): pool = CharacterPool() # doubles password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.symbols ) assert normal_pool_of_unique_characters(password) == 26 + 32 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.uppercase ) assert normal_pool_of_unique_characters(password) == len(pool.letters) assert normal_pool_of_unique_characters(password) == 26 + 26 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.lowercase | pool.numbers ) assert normal_pool_of_unique_characters(password) == 26 + 10 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.uppercase | pool.symbols ) assert normal_pool_of_unique_characters(password) == 26 + 32 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.uppercase | pool.numbers ) assert normal_pool_of_unique_characters(password) == 26 + 10 password = "******" assert normal_pool_of_unique_characters(password) == len( pool.uppercase | pool.numbers ) assert normal_pool_of_unique_characters(password) == 26 + 10
def test_normal_evaluation_singles(): pool = CharacterPool() # singles password = "******" assert normal_pool_of_unique_characters(password) == len(pool.lowercase) assert normal_pool_of_unique_characters(password) == 26 password = "******" assert normal_pool_of_unique_characters(password) == len(pool.uppercase) assert normal_pool_of_unique_characters(password) == 26 password = "******" assert normal_pool_of_unique_characters(password) == len(pool.symbols) assert normal_pool_of_unique_characters(password) == 32 password = "******" assert normal_pool_of_unique_characters(password) == len(pool.numbers) assert normal_pool_of_unique_characters(password) == 10 password = "******" assert normal_pool_of_unique_characters(password) == len(pool.whitespace) assert normal_pool_of_unique_characters(password) == 1
def test_password_policy(): # check object policy = PasswordPolicy() assert policy assert isinstance(policy, PasswordPolicy) # check attrs assert policy.lowercase == 0 assert policy.uppercase == 0 assert policy.symbols == 0 assert policy.numbers == 0 assert policy.whitespace == 0 assert policy.other == 0 assert policy.min_length == 12 assert policy.max_length == 128 assert policy.min_entropy == 32 assert policy.classification == "Weak" assert policy.forbidden_words == [] assert policy.pool assert policy.to_dict() assert isinstance(policy.to_dict(), dict) assert policy.to_dict() == dict( lowercase=0, uppercase=0, symbols=0, numbers=0, whitespace=0, other=0, min_length=12, max_length=128, entropy=32, classification="Weak", forbidden_words=[], character_pool=CharacterPool().to_dict(), )
def test_lenient_evaluation(): pool = CharacterPool() assert lenient_pool_of_unique_characters() == len(pool.all) assert lenient_pool_of_unique_characters() == 95
def hex_pool(): pool = CharacterPool(lowercase="abcdef", uppercase="ABCDEF", symbols="", whitespace="") yield pool
def __init__( self, lowercase: int = 0, uppercase: int = 0, symbols: int = 0, numbers: int = 0, whitespace: int = 0, other: int = 0, min_length: int = 12, max_length: int = 128, min_entropy: typing.Union[int, float] = 32, forbidden_words: list = None, character_pool: CharacterPool = None, requirement_cls: PasswordRequirement = None, classifier: Classifier = None, ): # set character pool if not passed if character_pool is None: self.pool = CharacterPool() else: self.pool = character_pool # set requirement class if not passed if requirement_cls is None: self.requirement_cls = PasswordRequirement else: self.requirement_cls = requirement_cls # check lowercase value acceptable # first check is int assert isinstance( lowercase, int ), "lowercase (the minimum number of lowercase characters) must be int" # then check it is a value between 0 and the number of # lowercase characters in the character pool (ascii is 0-26) assert 0 <= lowercase <= len(self.pool.lowercase), ( f"lowercase (the minimum number of lowercase characters) must be " f"between 0 and {len(self.pool.lowercase)} inclusive") self.lowercase = lowercase self.lowercase_requirement = MakePasswordRequirement( "the minimum number of lowercase characters", self.lowercase, cls=requirement_cls, ) # check uppercase value acceptable # first check is int assert isinstance( uppercase, int ), "uppercase (the minimum number of uppercase characters) must be int" # then check it is a value between 0 and the number of # lowercase characters in the character pool (ascii is 0-26) assert 0 <= uppercase <= len(self.pool.uppercase), ( f"uppercase (the minimum number of uppercase characters) must be " f"between 0 and {len(self.pool.uppercase)} inclusive") self.uppercase = uppercase self.uppercase_requirement = MakePasswordRequirement( "the minimum number of uppercase characters", self.uppercase, cls=requirement_cls, ) # check numbers value acceptable # first check is int assert isinstance( numbers, int ), "numbers (the minimum number of number characters) must be int" # then check it is a value between 0 and the number of # number characters in the character pool (ascii is 0-9) assert 0 <= numbers <= len(self.pool.numbers), ( f"numbers (the minimum number of number characters) must be " f"between 0 and {len(self.pool.numbers)} inclusive") self.numbers = numbers self.numbers_requirement = MakePasswordRequirement( "the minimum number of number characters", self.numbers, cls=requirement_cls) # check symbols value acceptable # first check is int assert isinstance( symbols, int ), "symbols (the minimum number of symbol characters) must be int" # then check it is a value between 0 and the number of # number characters in the character pool (ascii is 0-32) # although for symbols this is debatable, some might not include # particular symbol characters like '\' or ';', etc # this is overridable, like all pool features depending on use case assert 0 <= symbols <= len(self.pool.symbols), ( f"symbols (the minimum number of symbol characters) must be " f"between 0 and {len(self.pool.symbols)} inclusive") self.symbols = symbols self.symbols_requirement = MakePasswordRequirement( "the minimum number of symbol characters", self.symbols, cls=requirement_cls) # check whitespace value acceptable # first check is int assert isinstance(whitespace, int), ( "whitespace (the minimum number of whitespace characters) must be " "int") # then check it is a value between 0 and the number of # number characters in the character pool (ascii is 0-5) assert 0 <= whitespace <= len(self.pool.whitespace), ( f"whitespace (the minimum number of whitespace characters) must be " f"between 0 and {len(self.pool.whitespace)} inclusive") self.whitespace = whitespace self.whitespace_requirement = MakePasswordRequirement( "the minimum number of whitespace characters", self.whitespace, cls=requirement_cls, ) # check other value acceptable # first check is int assert isinstance( other, int), "other (the minimum number of other characters) must be int" # then check it is a value between 0 and the number of # other characters in the character pool (ascii is 0) # this can be used as a bucket by developers that want to allow # other characters assert 0 <= other <= len(self.pool.other), ( f"other (the minimum number of other characters) must be " f"between 0 and {len(self.pool.other)} inclusive") self.other = other self.other_requirement = MakePasswordRequirement( "the minimum number of other characters", self.other, cls=requirement_cls) # check min_length value acceptable # check is int assert isinstance( min_length, int), "min_length (the minimum password length) must be int" # check max_length value acceptable # check is int assert isinstance( max_length, int), "max_length (the maximum password length) must be int" # then check one is assert 0 <= min_length <= max_length, ( "the min_length (minimum password length) cannot be smaller than " "the max_length (maximum password length) and must be larger than " "0. However the min_length and max_length can be equal if the user " "desires a single length for all passwords") self.min_length = min_length self.min_length_requirement = MakePasswordRequirement( "the minimum password length", self.min_length, cls=requirement_cls, ) self.max_length = max_length self.max_length_requirement = MakePasswordRequirement( "the maximum password length", self.max_length, func=less_than_or_equal_to, cls=requirement_cls, ) assert isinstance(min_entropy, (int, float)), "entropy must be an int or float" assert 0 < min_entropy, "entropy must be greater than 0" self.min_entropy = min_entropy self.entropy_requirement = MakePasswordRequirement("entropy", self.min_entropy, cls=requirement_cls) self.forbidden_words = forbidden_words if forbidden_words else [] assert isinstance(self.forbidden_words, list), "forbidden words must be a list" for word in self.forbidden_words: assert isinstance(word, str), "all forbidden words must be strings" self.forbidden_words_requirements = MakePasswordRequirement( "forbidden words", self.forbidden_words, cls=requirement_cls, func=not_in, ) # set a classifier if not passed # with default values of: # "Very Weak" is entropy between 0 to 28 # "Weak" is entropy between 28 to 35 # "Ok" is entropy between 35 to 59 # "Good" is entropy between 59 to 127 # "Very Good" is entropy above 127 if classifier is None: self.classifier = Classifier() else: self.classifier = classifier # set a classification level from the entropy value self.classification = self.classifier.classify(self.min_entropy)
class PasswordPolicy: """ The password policy is where one can define what they expect of a password when submitted by a user. The default policy, is that a user chooses a password of at least 12 characters, but there is no requirement to use an amount of particular character types, e.g. symbols :param lowercase (int): the minimum number of lowercase characters in a password :param uppercase (int): the minimum number of uppercase characters in a password :param symbols (int): the minimum number of symbol characters in a password :param numbers (int): the minimum number of number characters in a password :param other (int): the minimum number of other characters in a password :param whitespace (int): the minimum number of whitespace characters in a password :param min_length (int): the minimum length for a password :param max_length (int): the maximum length for a password :param forbidden_words (list(str)): a list of forbidden words as strings :param character_pool (CharacterPool): the pool or characters to pick from """ def __init__( self, lowercase: int = 0, uppercase: int = 0, symbols: int = 0, numbers: int = 0, whitespace: int = 0, other: int = 0, min_length: int = 12, max_length: int = 128, min_entropy: typing.Union[int, float] = 32, forbidden_words: list = None, character_pool: CharacterPool = None, requirement_cls: PasswordRequirement = None, classifier: Classifier = None, ): # set character pool if not passed if character_pool is None: self.pool = CharacterPool() else: self.pool = character_pool # set requirement class if not passed if requirement_cls is None: self.requirement_cls = PasswordRequirement else: self.requirement_cls = requirement_cls # check lowercase value acceptable # first check is int assert isinstance( lowercase, int ), "lowercase (the minimum number of lowercase characters) must be int" # then check it is a value between 0 and the number of # lowercase characters in the character pool (ascii is 0-26) assert 0 <= lowercase <= len(self.pool.lowercase), ( f"lowercase (the minimum number of lowercase characters) must be " f"between 0 and {len(self.pool.lowercase)} inclusive") self.lowercase = lowercase self.lowercase_requirement = MakePasswordRequirement( "the minimum number of lowercase characters", self.lowercase, cls=requirement_cls, ) # check uppercase value acceptable # first check is int assert isinstance( uppercase, int ), "uppercase (the minimum number of uppercase characters) must be int" # then check it is a value between 0 and the number of # lowercase characters in the character pool (ascii is 0-26) assert 0 <= uppercase <= len(self.pool.uppercase), ( f"uppercase (the minimum number of uppercase characters) must be " f"between 0 and {len(self.pool.uppercase)} inclusive") self.uppercase = uppercase self.uppercase_requirement = MakePasswordRequirement( "the minimum number of uppercase characters", self.uppercase, cls=requirement_cls, ) # check numbers value acceptable # first check is int assert isinstance( numbers, int ), "numbers (the minimum number of number characters) must be int" # then check it is a value between 0 and the number of # number characters in the character pool (ascii is 0-9) assert 0 <= numbers <= len(self.pool.numbers), ( f"numbers (the minimum number of number characters) must be " f"between 0 and {len(self.pool.numbers)} inclusive") self.numbers = numbers self.numbers_requirement = MakePasswordRequirement( "the minimum number of number characters", self.numbers, cls=requirement_cls) # check symbols value acceptable # first check is int assert isinstance( symbols, int ), "symbols (the minimum number of symbol characters) must be int" # then check it is a value between 0 and the number of # number characters in the character pool (ascii is 0-32) # although for symbols this is debatable, some might not include # particular symbol characters like '\' or ';', etc # this is overridable, like all pool features depending on use case assert 0 <= symbols <= len(self.pool.symbols), ( f"symbols (the minimum number of symbol characters) must be " f"between 0 and {len(self.pool.symbols)} inclusive") self.symbols = symbols self.symbols_requirement = MakePasswordRequirement( "the minimum number of symbol characters", self.symbols, cls=requirement_cls) # check whitespace value acceptable # first check is int assert isinstance(whitespace, int), ( "whitespace (the minimum number of whitespace characters) must be " "int") # then check it is a value between 0 and the number of # number characters in the character pool (ascii is 0-5) assert 0 <= whitespace <= len(self.pool.whitespace), ( f"whitespace (the minimum number of whitespace characters) must be " f"between 0 and {len(self.pool.whitespace)} inclusive") self.whitespace = whitespace self.whitespace_requirement = MakePasswordRequirement( "the minimum number of whitespace characters", self.whitespace, cls=requirement_cls, ) # check other value acceptable # first check is int assert isinstance( other, int), "other (the minimum number of other characters) must be int" # then check it is a value between 0 and the number of # other characters in the character pool (ascii is 0) # this can be used as a bucket by developers that want to allow # other characters assert 0 <= other <= len(self.pool.other), ( f"other (the minimum number of other characters) must be " f"between 0 and {len(self.pool.other)} inclusive") self.other = other self.other_requirement = MakePasswordRequirement( "the minimum number of other characters", self.other, cls=requirement_cls) # check min_length value acceptable # check is int assert isinstance( min_length, int), "min_length (the minimum password length) must be int" # check max_length value acceptable # check is int assert isinstance( max_length, int), "max_length (the maximum password length) must be int" # then check one is assert 0 <= min_length <= max_length, ( "the min_length (minimum password length) cannot be smaller than " "the max_length (maximum password length) and must be larger than " "0. However the min_length and max_length can be equal if the user " "desires a single length for all passwords") self.min_length = min_length self.min_length_requirement = MakePasswordRequirement( "the minimum password length", self.min_length, cls=requirement_cls, ) self.max_length = max_length self.max_length_requirement = MakePasswordRequirement( "the maximum password length", self.max_length, func=less_than_or_equal_to, cls=requirement_cls, ) assert isinstance(min_entropy, (int, float)), "entropy must be an int or float" assert 0 < min_entropy, "entropy must be greater than 0" self.min_entropy = min_entropy self.entropy_requirement = MakePasswordRequirement("entropy", self.min_entropy, cls=requirement_cls) self.forbidden_words = forbidden_words if forbidden_words else [] assert isinstance(self.forbidden_words, list), "forbidden words must be a list" for word in self.forbidden_words: assert isinstance(word, str), "all forbidden words must be strings" self.forbidden_words_requirements = MakePasswordRequirement( "forbidden words", self.forbidden_words, cls=requirement_cls, func=not_in, ) # set a classifier if not passed # with default values of: # "Very Weak" is entropy between 0 to 28 # "Weak" is entropy between 28 to 35 # "Ok" is entropy between 35 to 59 # "Good" is entropy between 59 to 127 # "Very Good" is entropy above 127 if classifier is None: self.classifier = Classifier() else: self.classifier = classifier # set a classification level from the entropy value self.classification = self.classifier.classify(self.min_entropy) def to_dict(self) -> dict: rv = { "lowercase": self.lowercase, "uppercase": self.uppercase, "symbols": self.symbols, "numbers": self.numbers, "whitespace": self.whitespace, "other": self.other, "min_length": self.min_length, "max_length": self.max_length, "entropy": self.min_entropy, "forbidden_words": self.forbidden_words, "classification": self.classification, "character_pool": self.pool.to_dict(), } return rv def test_password(self, password: str, failures_only: bool = True): password = _make_password(password) validity = [ self.lowercase_requirement(password.lowercase), self.uppercase_requirement(password.uppercase), self.numbers_requirement(password.numbers), self.symbols_requirement(password.symbols), self.whitespace_requirement(password.whitespace), self.other_requirement(password.other), self.min_length_requirement(password.length), self.max_length_requirement(password.length), self.entropy_requirement(password.entropy), self.forbidden_words_requirements(password.password), ] return [i for i in validity if not i] if failures_only else validity def validate(self, password): return not bool(self.test_password(password))