Commits

Anonymous committed d408dbc

Initial Release

  • Participants

Comments (0)

Files changed (7)

+.coverage
+hashish.egg-info
+hashishenv
+*.pyc
+Initial Release
+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):
+        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):
+        if not hasattr(self, "_salt") or self._salt is None:
+            self._salt = self._generate_salt()
+
+        return self._salt
+
+    @salt.setter
+    def salt(self, value):
+        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):
+        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__)))
+Hashish
+=======
+
+A simple mixin that provides password hashing functionality.  It uses a
+configurable salt count to set a field.  It is designed to be easy to use
+in most instances
+
+Example Usage with SqlAlchemy
+-----------------------------
+
+    :::python
+    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.
+[nosetests]
+match=^test
+nocapture=0
+cover-package=hashish
+with-coverage=1
+cover-erase=1
+verbosity=2
+import os
+
+from setuptools import setup
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'readme.md')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+]
+
+development_requires = [
+    'nose',
+    'flake8',
+    'coverage',
+]
+
+setup(name='hashish',
+      version='0.1',
+      description='Password Hashing Library',
+      long_description=README + '\n\n' +  CHANGES,
+      py_modules=['hashish'],
+      author='Russell Hay',
+      author_email='me@russellhay.com',
+      url='https://bitbucket.org/russellhay/hashish',
+      keywords='',
+      include_package_data=True,
+      zip_safe=False,
+      test_suite='tests',
+      install_requires=requires,
+      tests_require=development_requires,
+      )
+from nose import tools as t
+
+import hashish
+
+class _TestObject(hashish.HashishMixin):
+    __PASSWORD_FIELD__ = "password_hash"
+    __SALT_COUNT__ = 4
+
+    def __init__(self):
+        self.password_hash = None
+
+    @property
+    def password_field(self):
+        return self.__PASSWORD_FIELD__
+
+    @property
+    def salt_count(self):
+        return self.__SALT_COUNT__
+
+def test_functional():
+    obj = _TestObject()
+
+    t.eq_(obj.password_hash, None, "Password Hash not initialized")
+    t.eq_(obj.password_field, "password_hash")
+    t.eq_(obj.salt_count, 4)
+    obj.password = "1234"
+    t.assert_is_not_none(obj.password_hash,
+        "Password Hash was not generated when password was set")
+    t.assert_not_equal(obj.password_hash, "1234",
+        "Password did not hash properly")
+    t.eq_(obj.password_hash[:5], "{0};".format(obj.salt))
+
+def test_salt_value():
+    obj = _TestObject()
+
+    t.eq_(len(obj.salt), obj.salt_count)
+
+@t.raises(AttributeError)
+def test_cannot_read_password():
+    obj = _TestObject()
+    print obj.password
+
+@t.raises(ValueError)
+def test_salt_too_long():
+    obj = _TestObject()
+
+    obj.salt="12345"
+
+@t.raises(ValueError)
+def test_salt_too_short():
+    obj = _TestObject()
+    obj.salt="123"
+
+def test_salt_regeneration():
+    obj = _TestObject()
+    salt1 = obj.salt
+
+    obj.salt = None
+    salt2 = obj.salt
+
+    t.assert_not_equal(salt1, salt2)
+
+def test_setting_salt():
+    obj = _TestObject()
+
+    obj.salt = "1234"
+    t.eq_(obj.salt, "1234")
+
+def test_validate_valid():
+    obj = _TestObject()
+
+    obj.password = "1234"
+    t.ok_(obj.validate("1234"))
+
+def test_validate_invalid_no_exception():
+    obj = _TestObject()
+
+    obj.password = "1234"
+    t.ok_(not obj.validate("12345"))
+
+@t.raises(hashish.InvalidPasswordError)
+def test_validate_invalid_exception():
+    obj = _TestObject()
+
+    obj.password = "1234"
+    obj.validate("12345", throw=True)