Source

hashish / hashish.py

Full commit
"""
.. module:: hashish
   :platform: Unix, Windows
   :synopsis: A small mixin module that provides password hashing

.. moduleauthor:: Russell Hay <me@russellhay.com>

"""

import string
import random
import hashlib

SALT_CHARACTERS = string.letters + string.digits

class InvalidPasswordError(Exception):
    """ Invalid Password was provided to the validate method """

class HashishMixin(object):
    """ HashishMixin adds pasword hashing to an object.

    Example Usage with SqlAlchemy:

        class Account(Base,HashishMixin):
            __PASSWORD_FIELD__ = "password_hash"  # Defaults to "hash"
            __SALT_COUNT__ = 4  # Defaults to 2

            password_hash = Column(String)


        test_account = Account()
        test_account.password = "SuperSecretPasswordYo!"
        test_account.validate("SuperSecretPasswordYo!")  # True if the hashes match using the same salt.

    This updates the password_hash field with a hashed password
    """

    __PASSWORD_FIELD__ = "hash"
    __SALT_COUNT__ = 2

    @property
    def password(self):
        raise AttributeError("Password is not readable")

    @password.setter
    def password(self, value):
        """ Generate the password has by setting this property

        :param value: The data to be hashed, most-likely the password.
        :type value: str.

        """
        salt = self.salt
        data = "{salt};{value}".format(salt=salt,
                                       value=self._generate_hash(salt, value))
        setattr(self, self.__PASSWORD_FIELD__, data)

    @property
    def salt(self):
        """ The password salt, if it doesn't exist, one is generated

        :returns: string -- a salt value

        """
        if not hasattr(self, "_salt") or self._salt is None:
            self._salt = self._generate_salt()

        return self._salt

    @salt.setter
    def salt(self, value):
        """ The password salt

        :param value: The salt, must be __SALT_COUNT__ characters in length.
        :param type: str.
        :raises: ValueError

        """
        if value is None:
            self._salt = None
            return

        if len(value) != self.__SALT_COUNT__:
            raise ValueError(
                "Salt must be {0} characters".format(self.__SALT_COUNT__))

        self._salt = str(value)

    def validate(self, value, throw=False):
        """ Validate that a password matches the password hash

        :param value: The value to check.
        :type value: str.
        :param throw: Throw an exception on invalid password
        :type throw: bool.
        :returns: bool -- Is the password matches or not
        :raises: InvalidPasswordError

        """
        hash_value = getattr(self, self.__PASSWORD_FIELD__)
        salt = hash_value[:self.__SALT_COUNT__]
        expected = hash_value[(self.__SALT_COUNT__+1):]

        actual = self._generate_hash(salt, value)

        if throw and actual != expected:
            raise InvalidPasswordError()

        return actual == expected

    def _generate_hash(self, salt, value):
        hash_string = "{salt};{value}".format(salt=salt, value=value)
        return hashlib.sha512(hash_string).hexdigest()

    def _generate_salt(self):
        return "".join((random.choice(SALT_CHARACTERS) for x in range(self.__SALT_COUNT__)))