Commits

Kelvin Wong committed a85b22d

Backwards incompatible change as N is no longer stored in the hash, instead the log base 2 of N. More doc work. Test for length of hash less than 128

Comments (0)

Files changed (4)

-=============
 Django-Scrypt
-=============
+*************
 
-**WARNING**
+Djando-Scrypt is a Scrypt-enabled password hasher for Django 1.4
 
-This is alpha software under active development. It was only tested
-on **Python 2.7**. It probably will not run on Python 2.5 since
-*py-scrypt* doesn't run on anything earlier than Python 2.6.
+.. warning::
 
-Basic Usage
-~~~~~~~~~~~
+   This is alpha software under active development. It was tested only
+   on **Python 2.7**. It probably will not run on Python 2.5 since
+   ``py-scrypt`` doesn't run on interpreters earlier than Python 2.6.
 
-To use Scrypt as your default storage algorithm in Django 1.4, do the
-following:
+Installation
+============
 
-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``).
+.. note::
 
-**Py-Scrypt 0.5.5 contains a major bug on 64-bit Linux**
+   You need to install Django 1.4 and ``py-scrypt`` prior to installing
+   Django-Scrypt
 
-Next, install Django-Scrypt::
+Using source tarballs
+---------------------
+
+1. Download the source tarball for Django-Scrypt from Pypi::
+
+  http://pypi.python.org/pypi/django-scrypt/
+
+2. Decompress it and make it your working directory::
+
+  $ tar zxvf django-scrypt-0.1.1.tar.gz
+  $ cd django-scrypt-0.1.1
+
+3. Install it into your site-packages (if you install to the system's site
+packages you will probably need to be root or use ``sudo``)::
 
   $ python setup.py install
 
-Then, run the test suite::
+4. Test your installation::
 
   $ python setup.py test
 
-In your Django 1.4 application *settings.py* file, modify (or add) the
-``PASSWORD_HASHERS`` tuple to include ``ScryptPasswordHasher`` first.
+Using Pip and Pypi
+------------------
+
+1. Use the ``pip`` command to install from Pypi::
+
+  $ pip install django-scrypt
+
+If you are installing to the system-wide site-packages then you will probably need to be root or use ``sudo``.
+
+Basic Usage
+===========
+
+.. warning::
+
+   This software depends on ``py-scrypt`` version 0.5.5 to reveal
+   the Scrypt hashing function. Unfortunately, ``py-scrypt`` contains a bug
+   that can result in incorrect hashing when run on 64-bit Linux systems. View
+   the ``py-scrypt`` issue tracker for the latest information on this issue.
+
+To use Scrypt as your default password storage algorithm in Django 1.4,
+install it and make the following changes. In your Django 1.4 application
+*settings.py* file, modify (or add) the ``PASSWORD_HASHERS`` tuple to include
+``ScryptPasswordHasher`` as the first hasher in the tuple. It needs to be at
+the top.
 
 For example::
 
   )
 
 Note: You need to keep the other hasher entries in this list, or else Django
-won't be able to upgrade passwords!
+won't be able to upgrade the passwords!
 
 You have now changed your app to use Scrypt as the default storage algorithm.
 
+As users login to your system they will automatically upgrade to use Scrypt
+hashes.
+
+Caveat
+======
+
+Django Password Field Character Length Limits
+---------------------------------------------
+
+By default, Django limits password field lengths to 128 characters. Using the
+default settings in Django-Scrypt with the Django salting implementation
+should yield encoded hashes less than 128 characters; however, if you override
+the ScryptPasswordHasher class variables you can end up overflowing the field.
+
+The solution is to increase the size of the password field (this example uses
+256 characters but it can be larger). You can do this using the django shell
+from your project root::
+
+  $ cd ~/my_django_project_root_with_manage_py_file_in_it
+  $ python manage.py shell
+  Python 2.7.3 (default, May  4 2012, 11:07:18)
+  [GCC 4.0.1 (Apple Inc. build 5493)] on darwin
+  Type "help", "copyright", "credits" or "license" for more information.
+  (InteractiveConsole)
+  >>> from django.contrib.auth import models
+  >>> pf = models.User._meta.get_field('password')
+  >>> pf
+  <django.db.models.fields.CharField: password>
+  >>> pf.max_length
+  128
+  >>> pf.max_length = max(pf.max_length, 256)
+  >>> pf.max_length
+  256
+  >>>
+
 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.
 
+https://bitbucket.org/kelvinwong_ca/django-scrypt/issues
+
+Thank-you
+=========
+
+Thank-you for taking the time to evaluate this software. I appreciate
+receiving feedback on your experiences with it and I welcome code
+contributions and development ideas.
+
 http://www.kelvinwong.ca/coders
 
-https://bitbucket.org/kelvinwong_ca/django-scrypt/issues
-
+Thanks to Dr Colin Percival for his original Scrypt software,
+also to Magnus Hallin for the py-scrypt Python module.

django_scrypt/hashers.py

 from django.utils.datastructures import SortedDict
 from django.utils.crypto import constant_time_compare
 from django.utils.translation import ugettext_noop as _
+
 import scrypt
 
 
 
     algorithm = "scrypt"
     library = ("scrypt")
-    N = 16384
+    Nexp = 14  # N == 2 ** 14 == 16384
     r = 8
     p = 1
     buflen = 64
         """
         Checks if the given password is correct
         """
-        algorithm, salt, N, r, p, buflen, h = encoded.split('$')
+        algorithm, salt, Nexp, 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))
+        # TODO: buflen is an experimental proposal in py-scrypt
+        hashp = self.encode(password, salt, int(Nexp), int(r), int(p))
         return constant_time_compare(encoded, hashp)
 
-    def encode(self, password, salt, N=None, r=None, p=None, buflen=None):
+    def encode(self, password, salt, Nexp=None, r=None, p=None, buflen=None):
         """
-        Creates an encoded database value
+        Creates an encoded hash string from password, salt and optional parameters
 
-        The result is normally formatted as "algorithm$salt$hash" and
-        must be fewer than 128 characters.
+        password is the user's chosen password as a string
+        salt is a string, django provides a 12-character random string from
+            [a-zA-Z0-9] by default
+        Nexp is the exponent for N such that N = 2 ** Nexp, Nexp = 14 means
+            N = 2 ** 14 = 16384 which is the value passed to the
+            Scrypt module. Must be a positive integer >= 1.
+        r is the r-value passed to Scrypt
+        p is the p-value passed to Scrypt
+        buflen is the length of the returned hash in bytes (not currently
+            implemented in underlying module)
+
+        Returns "scrypt$salt$Nexp$r$p$buflen$hash" where hash is a base64
+        encoded byte string (64-bytes by default)
         """
         assert password
         assert salt and '$' not in salt
         hashed = [self.algorithm]
         hashed.append(salt)
-        if not N:
-            N = self.N
+        if not Nexp:
+            Nexp = self.Nexp
         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
+        # TODO: buflen is an experimental proposal in py-scrypt
         buflen = self.buflen
-        hashed.append(str(N))
+        hashed.append(str(Nexp))
         hashed.append(str(r))
         hashed.append(str(p))
         hashed.append(str(buflen))
-        h = scrypt.hash(password, salt, N, r, p)
+        h = scrypt.hash(password, salt, 2 ** Nexp, r, p)
         hashed.append(h.encode('base64').strip())
         return "$".join(hashed)
 
         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('$')
+        algorithm, salt, Nexp, r, p, buflen, h = encoded.split('$')
         assert algorithm == self.algorithm
         return SortedDict([
             (_('algorithm'), algorithm),
             (_('salt'), mask_hash(salt, show=2)),
-            (_('N'), N),
+            (_('Nexp'), Nexp),
             (_('r'), r),
             (_('p'), p),
             (_('buflen'), buflen),
 cmdclasses['test'] = Tester
 
 setup(name='django-scrypt',
-      version='0.1.0',
+      version='0.1.1',
       description='A Scrypt-enabled password hasher for Django 1.4',
       long_description=long_description,
       author='Kelvin Wong',

tests/test_django_scrypt.py

         encoded = make_password(self.password)
         self.assertTrue(check_password(self.password, encoded))
 
+    def test_encoder_hash_less_than_128_characters(self):
+        """Test that returned encoded hash is less than db limit of 128 characters"""
+        encoded = make_password(self.password)
+        self.assertTrue(len(encoded) < 128)
+
     def test_encoder_specified_scrypt_hasher(self):
         """Test hasher is obtained by name"""
         encoded = make_password('letmein', hasher='scrypt')
     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('$')
+        algorithm, salt, Nexp, r, p, buflen, h = encoded.split('$')
         self.assertEqual(algorithm, self.expected_hash_prefix)
         self.assertTrue(len(salt))
-        self.assertTrue(N.isdigit())
+        self.assertTrue(Nexp.isdigit())
         self.assertTrue(r.isdigit())
         self.assertTrue(p.isdigit())
         self.assertTrue(buflen.isdigit())
         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[_('Nexp')].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'"""