1. Russell Hay
  2. hashish


hashish / hashish.py

.. 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

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

    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)

    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

    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

        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)

        comparison = [ord(a) ^ ord(b) for a,b in zip(actual,expected)]
        valid_password = sum(comparison) == 0

        if throw and not valid_password:
            raise InvalidPasswordError()

        return valid_password

    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__)))