1. barry_allard
  2. django-scrypt

Commits

Kelvin Wong  committed f47659e

Initial working code

  • Participants
  • Branches default

Comments (0)

Files changed (8)

File .hgignore

View file
+.pyc
+dev*.db
+local_settings.py
+.egg
+MANIFEST
+build
+dist
+.swp
+.lock
+.pid
+.DS_Store
+.svn
+.xml
+.sqlite
+.egg-info

File LICENSE

View file
+Copyright © 2012 Kelvin Wong <code@kelvinwong.ca>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL "KELVIN WONG" BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File README.rst

View file
+=============
+Django-Scrypt
+=============
+
+** WARNING **
+
+This is alpha software under active development. It was tested only
+on **Python 2.7**. It probably will not run on less than Python 2.6 since
+py-scrypt doesn't run on anything earlier than Python 2.6.
+
+Basic Usage
+~~~~~~~~~~~
+
+To use Scrypt as your default storage algorithm in Django 1.4, do the
+following:
+
+Install the py-scrypt library version 0.5.5 or later (probably by running ``sudo pip install py-scrypt``, or downloading the library and installing it with ``python setup.py install``).
+
+**Py-Scrypt 0.5.5 contains a major bug on 64-bit Linux**
+
+Install Django-Scrypt::
+
+  $ python setup.py install
+
+Run the test suite::
+
+  $ python setup.py test
+
+In your application *settings.py* file, modify (or add) the ``PASSWORD_HASHERS`` tuple to include ``ScryptPasswordHasher`` first. For example::
+
+PASSWORD_HASHERS = (
+    'django_scrypt.hashers.ScryptPasswordHasher',
+    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+    'django.contrib.auth.hashers.SHA1PasswordHasher',
+    'django.contrib.auth.hashers.MD5PasswordHasher',
+    'django.contrib.auth.hashers.CryptPasswordHasher',
+)
+
+Note: You need to keep the other entries in this list, or else Django won't be able to upgrade passwords!
+
+You have now changed your app to use Scrypt as the default storage algorithm.
+
+More Stuff?
+~~~~~~~~~~~
+
+There is a bit more to the software, but you will have to read the source to
+figure it out. :)
+
+Bugs! Help!!
+~~~~~~~~~~~~
+
+If you find bugs please report them to the BitBucket issue tracker or send
+me an email to code@kelvinwong.ca. Any serious security bugs should be
+reported via email.
+
+http://www.kelvinwong.ca/coders
+
+
+

File django_scrypt/__init__.py

Empty file added.

File django_scrypt/hashers.py

View file
+from django.contrib.auth.hashers import BasePasswordHasher, mask_hash
+from django.utils.datastructures import SortedDict
+from django.utils.crypto import constant_time_compare
+from django.utils.translation import ugettext_noop as _
+import scrypt
+
+
+class ScryptPasswordHasher(BasePasswordHasher):
+    """
+    Secure password hashing using the scrypt algorithm
+
+    The py-scrypt library must be installed separately. That library
+    depends on native C code and might cause portability issues.
+    """
+
+    algorithm = "scrypt"
+    library = ("scrypt")
+    N = 16384
+    r = 8
+    p = 1
+    buflen = 64
+
+    def verify(self, password, encoded):
+        """
+        Checks if the given password is correct
+        """
+        algorithm, salt, N, r, p, buflen, h = encoded.split('$')
+        assert algorithm == self.algorithm
+        # TODO: bufflen is an experimental proposal of py-scrypt, not supported in this version
+        hashp = self.encode(password, salt, int(N), int(r), int(p))
+        return constant_time_compare(encoded, hashp)
+
+    def encode(self, password, salt, N=None, r=None, p=None, buflen=None):
+        """
+        Creates an encoded database value
+
+        The result is normally formatted as "algorithm$salt$hash" and
+        must be fewer than 128 characters.
+        """
+        assert password
+        assert salt and '$' not in salt
+        hashed = [self.algorithm]
+        hashed.append(salt)
+        if not N:
+            N = self.N
+        if not r:
+            r = self.r
+        if not p:
+            p = self.p
+        if not buflen:
+            buflen = self.buflen
+        # TODO: bufflen is an experimental proposal of py-scrypt, not supported in this version
+        buflen = self.buflen
+        hashed.append(str(N))
+        hashed.append(str(r))
+        hashed.append(str(p))
+        hashed.append(str(buflen))
+        h = scrypt.hash(password, salt, N, r, p)
+        hashed.append(h.encode('base64').strip())
+        return "$".join(hashed)
+
+    def safe_summary(self, encoded):
+        """
+        Returns a summary of safe values
+
+        The result is a dictionary and will be used where the password field
+        must be displayed to construct a safe representation of the password.
+        """
+        algorithm, salt, N, r, p, buflen, h = encoded.split('$')
+        assert algorithm == self.algorithm
+        return SortedDict([
+            (_('algorithm'), algorithm),
+            (_('salt'), mask_hash(salt, show=2)),
+            (_('N'), N),
+            (_('r'), r),
+            (_('p'), p),
+            (_('buflen'), buflen),
+            (_('hash'), mask_hash(h)),
+        ])

File setup.py

View file
+#!/usr/bin/env python
+
+import os
+import sys
+from distutils.core import setup, Command
+
+cmdclasses = dict()
+README_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+                           'README.rst')
+long_description = open(README_PATH, 'r').read()
+
+
+class Tester(Command):
+    """Runs django-scrypt unit tests"""
+
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        from django.conf import settings
+        settings.configure(
+            USE_I18N=True
+        )
+        try:
+            from django.utils.unittest import TextTestRunner, defaultTestLoader
+        except ImportError:
+            print("Please install django to run the test suite")
+            exit(-1)
+        from tests import test_django_scrypt
+        suite = defaultTestLoader.loadTestsFromModule(test_django_scrypt)
+        runner = TextTestRunner()
+        result = runner.run(suite)
+
+cmdclasses['test'] = Tester
+
+setup(name='django-scrypt',
+      version='0.1.0',
+      description='Support for Scrypt hashing in Django 1.4',
+      long_description=long_description,
+      author='Kelvin Wong',
+      author_email='code@kelvinwong.ca',
+      url='https://bitbucket.org/kelvinwong_ca/django-scrypt',
+      classifiers=['Development Status :: 3 - Alpha',
+                   'Framework :: Django',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: BSD License',
+                   'Programming Language :: Python :: 2.6',
+                   'Programming Language :: Python :: 2.7',
+                   'Topic :: Security :: Cryptography',
+                   'Topic :: Software Development :: Libraries'],
+      packages=['django_scrypt'],
+      cmdclass=cmdclasses)

File tests/__init__.py

Empty file added.

File tests/test_django_scrypt.py

View file
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
+from django.contrib.auth.hashers import (
+    is_password_usable, check_password, make_password,
+    get_hasher, load_hashers, UNUSABLE_PASSWORD)
+from django.utils.unittest import TestCase, skipUnless
+from django.utils.translation import ugettext_noop as _
+
+from django_scrypt.hashers import ScryptPasswordHasher
+
+try:
+    import scrypt
+except ImportError:
+    scrypt = None
+
+
+@skipUnless(scrypt, "Uninstalled scrypt module needed to generate hash")
+class TestScrypt(TestCase):
+
+    def setUp(self):
+        scrypt_hashers = ("django_scrypt.hashers.ScryptPasswordHasher",) + default_hashers
+        load_hashers(password_hashers=scrypt_hashers)
+        self.password = 'letmein'
+        self.bad_password = 'letmeinz'
+        self.expected_hash_prefix = "scrypt"
+
+    def test_verify_bad_passwords_fail(self):
+        """Test verify method causes failure on mismatched passwords"""
+        encoded = make_password(self.password)
+        self.assertFalse(check_password(self.bad_password, encoded))
+
+    def test_verify_passwords_match(self):
+        """Test verify method functions via check_password"""
+        encoded = make_password(self.password)
+        self.assertTrue(check_password(self.password, encoded))
+
+    def test_encoder_specified_scrypt_hasher(self):
+        """Test hasher is obtained by name"""
+        encoded = make_password('letmein', hasher='scrypt')
+        self.assertTrue(check_password(self.password, encoded))
+
+    def test_encoder_hash_usable(self):
+        """Test encoder returns usable hash string"""
+        encoded = make_password(self.password)
+        self.assertTrue(is_password_usable(encoded))
+
+    def test_encoder_hash_starts_with_algorithm_string(self):
+        """Test that encoded hash string has correct prefix with first separator"""
+        encoded = make_password(self.password)
+        self.assertTrue(encoded.startswith(self.expected_hash_prefix + "$"))
+
+    def test_encoder_hash_has_required_sections(self):
+        """Test encoder returns hash with required sections"""
+        encoded = make_password(self.password)
+        algorithm, salt, N, r, p, buflen, h = encoded.split('$')
+        self.assertEqual(algorithm, self.expected_hash_prefix)
+        self.assertTrue(len(salt))
+        self.assertTrue(N.isdigit())
+        self.assertTrue(r.isdigit())
+        self.assertTrue(p.isdigit())
+        self.assertTrue(buflen.isdigit())
+        self.assertTrue(len(h))
+
+    def test_safe_summary_has_required_sections(self):
+        """Test safe_summary returns string with required informative sections"""
+        encoded = make_password(self.password)
+        hasher = get_hasher('scrypt')
+        d = hasher.safe_summary(encoded)
+        self.assertEqual(d[_('algorithm')], self.expected_hash_prefix)
+        self.assertTrue(len(d[_('salt')]))
+        self.assertTrue(d[_('N')].isdigit())
+        self.assertTrue(d[_('r')].isdigit())
+        self.assertTrue(d[_('p')].isdigit())
+        self.assertTrue(d[_('buflen')].isdigit())
+        self.assertTrue(len(d[_('hash')]))
+        #print(hasher.safe_summary(encoded))
+
+    def test_class_algorithm_string_matches_expected(self):
+        """Test django_scrypt algorithm string matches expected value 'scrypt'"""
+        self.assertEqual(ScryptPasswordHasher.algorithm, self.expected_hash_prefix)
+
+    def test_no_upgrade_on_incorrect_pass(self):
+        self.assertEqual('scrypt', get_hasher('default').algorithm)
+        for algo in ('sha1', 'md5'):
+            encoded = make_password('letmein', hasher=algo)
+            state = {'upgraded': False}
+
+            def setter():
+                state['upgraded'] = True
+            self.assertFalse(check_password(self.bad_password, encoded, setter))
+            self.assertFalse(state['upgraded'])
+
+    def test_no_upgrade(self):
+        encoded = make_password('letmein')
+        state = {'upgraded': False}
+
+        def setter():
+            state['upgraded'] = True
+        self.assertFalse(check_password(self.bad_password, encoded, setter))
+        self.assertFalse(state['upgraded'])
+
+    def test_upgrade(self):
+        self.assertEqual('scrypt', get_hasher('default').algorithm)
+        for algo in ('sha1', 'md5'):
+            encoded = make_password('letmein', hasher=algo)
+            state = {'upgraded': False}
+
+            def setter(password):
+                state['upgraded'] = True
+            self.assertTrue(check_password('letmein', encoded, setter))
+            self.assertTrue(state['upgraded'])