Commits

Kelvin Wong committed 2069c98

Tox setup. Testing subclassing of hasher. Doc additions. Added keywords to setup. Unicode text test. Add version string and test.

  • Participants
  • Parent commits 6aebaba

Comments (0)

Files changed (7)

 MANIFEST
 build
 dist
+.tox
+.vagrant
 .swp
 .lock
 .pid
 
 2. Decompress it and make it your working directory::
 
-       $ tar zxvf django-scrypt-0.2.1.tar.gz
-       $ cd django-scrypt-0.2.1
+       $ tar zxvf django-scrypt-0.2.2.tar.gz
+       $ cd django-scrypt-0.2.2
 
 3. Install it into your ``site-packages`` directory (if you install to the
    system's ``site-packages`` you will probably need to be root or you will
    You need to keep the other hasher entries in this list or else *Django*
    won't be able to upgrade the passwords!
 
+Scrypt Parameters
+-----------------
+
+*Scrypt* takes three tuning parameters: ``N``, ``r`` and ``p``.
+They affect memory usage and running time. Memory usage is approximately
+``128 * r * N`` bytes. [#]_ These are the default values::
+
+   Nexp = lb(N) = 14, r = 8 and p = 1
+   where lb is logarithm base 2
+
+*Django-Scrypt* stores ``Nexp`` in the encoded hash, but not ``N``. The positive integer ``Nexp`` is the exponent used to generate ``N`` which is calculated as needed (``N = 2 ** Nexp``). Doing this saves space in the database row. These default values lead to *Scrypt* using ``128 * 8 * 2 ** 14 = 16M`` bytes of memory.
+
+The values of ``N``, ``r`` and ``p`` affect running time proportionately; however, ``p`` can be used to independently tune the running time since it has a smaller influence on memory usage.
+
+The final parameter ``buflen`` has been proposed for *Py-Scrypt* but is not implemented. The value will be used to change the size of the returned hash. Currently, *Py-Scrypt's* ``hash`` function returns a message digest of length 64-bytes or 512-bits.
+
+.. [#] Adapted from Falko Peters' `Crypto.Scrypt package for Haskell  <http://hackage.haskell.org/packages/archive/scrypt/0.3.2/doc/html/Crypto-Scrypt.html>`_
+
 .. _Caveats:
 
 Caveats
 
         ``scrypt$salt$14$8$1$64$Base64Hash==``
 
-The good news is that 14 is three characters shorter than 16384. The bad news
+The good news is that "14" is three characters shorter than "16384". The bad news
 is that this introduces a backwards incompatible change as of version 0.2.0.
 
-If you see your application generating *HTTP 500 Server Errors* with a 
-*scrypt.error: 'hash parameters are wrong (r*p should be < 2**30, and N should
+If you see your application generating *HTTP 500 Server Errors* with an *Exception* raised with
+*error: 'hash parameters are wrong (r*p should be < 2**30, and N should
 be a power of two > 1)'* then you should suspect that an old hash is telling
 *Scrypt* to use ``N = 2 ** 16384`` which is way, way, way too large. The
 solution is to replace the 16384 in the old hashes with 14. You might have to alter your database manually or write some custom code to fix this change.

File django_scrypt/hashers.py

         Nexp -- 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 -- The r-value passed to Scrypt
-        p -- The p-value passed to Scrypt
+        r -- The r-value passed to Scrypt as a positive integer
+        p -- The p-value passed to Scrypt as a positive integer
         buflen -- Length of the returned hash in bytes (not implemented)
 
         Returns "scrypt$salt$Nexp$r$p$buflen$hash" where hash is a base64
         except ImportError:
             print("Please install Django => 1.4 to run the test suite")
             exit(-1)
-        from tests import test_django_scrypt
+        from tests import test_django_scrypt, test_subclassing
         suite = defaultTestLoader.loadTestsFromModule(test_django_scrypt)
+        suite.addTests(defaultTestLoader.loadTestsFromModule(test_subclassing))
         runner = TextTestRunner()
         result = runner.run(suite)
 
       version=__version__,
       description='A Scrypt-enabled password hasher for Django 1.4',
       long_description=long_description,
+      keywords=['Py-Scrypt', 'Scrypt', 'Django', 'Django-Scrypt'],
       author='Kelvin Wong',
       author_email='code@kelvinwong.ca',
       url='https://bitbucket.org/kelvinwong_ca/django-scrypt',

File tests/test_django_scrypt.py

 from django.utils.unittest import TestCase, skipUnless
 from django.utils.translation import ugettext_noop as _
 
+import django_scrypt
 from django_scrypt.hashers import ScryptPasswordHasher
 
 try:
         scrypt_hashers = ("django_scrypt.hashers.ScryptPasswordHasher",) + default_hashers
         load_hashers(password_hashers=scrypt_hashers)
         self.password = 'letmein'
+        self.unicode_text = '\xe1\x93\x84\xe1\x93\x87\xe1\x95\x97\xe1\x92\xbb\xe1\x92\xa5\xe1\x90\x85\xe1\x91\xa6'.decode('utf-8')
         self.bad_password = 'letmeinz'
         self.expected_hash_prefix = "scrypt"
         self.old_format_encoded_hash = "scrypt$FYY1dftUuK0b$16384$8$1$64$/JYOBEED7nMzJgvlqfzDj1JKGVLup0eYLyG39WA2KCywgnB1ubN0uzFYyaEQthINm6ynjjqr+D+U\nw5chi74WVw=="
         self.old_format_fix_encoded_hash = "scrypt$FYY1dftUuK0b$14$8$1$64$/JYOBEED7nMzJgvlqfzDj1JKGVLup0eYLyG39WA2KCywgnB1ubN0uzFYyaEQthINm6ynjjqr+D+U\nw5chi74WVw=="
         self.encoded_hash = "scrypt$gwQg9TZ3eyub$14$8$1$64$lQhi3+c0xkYDUj35BvS6jVTlHRAH/RS4nkpd1tKMc0r9PcFyjCjPj1k9/CkSCRvcTvHiWfFYpHfB\nZDCHMNIeHA=="
 
-    def test_default_encoder_hash_less_than_128_characters(self):
+    def test_version_string_set(self):
+        """Test for version string on package"""
+        self.assertTrue(type(django_scrypt.__version__), str)
+        self.assertTrue(len(django_scrypt.__version__) > 0)
+
+    def test_encoder_default_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_accepts_unicode(self):
+        """Test that passwords can be Unicode"""
+        encoded = make_password(self.unicode_text)
+        self.assertTrue(check_password(self.unicode_text, encoded))
+
     def test_encoder_specified_scrypt_hasher(self):
         """Test hasher is obtained by name"""
         encoded = make_password(self.password, hasher='scrypt')

File tests/test_subclassing.py

+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+import time
+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_scrypt.hashers import ScryptPasswordHasher
+
+try:
+    import scrypt
+except ImportError:
+    scrypt = None
+
+
+@skipUnless(scrypt, "Uninstalled scrypt module needed to generate hash")
+class TestBigMemNScryptHasher(TestCase):
+
+    def setUp(self):
+        scrypt_hashers = ("tests.test_subclassing.BigMemNScryptHasher",
+                          "django_scrypt.hashers.ScryptPasswordHasher") + default_hashers
+        load_hashers(password_hashers=scrypt_hashers)
+        self.password = 'letmein'
+
+    def test_BigMemScryptHasher(self):
+        """Test operation of the extended bigmem hasher"""
+        encoded = make_password(self.password, hasher='scrypt_N')
+        self.assertTrue(check_password(self.password, encoded))
+
+
+@skipUnless(scrypt, "Uninstalled scrypt module needed to generate hash")
+class TestBigMemScryptHasher(TestCase):
+
+    def setUp(self):
+        scrypt_hashers = ("tests.test_subclassing.BigMemScryptHasher",
+                          "django_scrypt.hashers.ScryptPasswordHasher") + default_hashers
+        load_hashers(password_hashers=scrypt_hashers)
+        self.password = 'letmein'
+
+    def test_BigMemScryptHasher(self):
+        """Test operation of the extended bigmem hasher"""
+        encoded = make_password(self.password, hasher='scrypt_r')
+        self.assertTrue(check_password(self.password, encoded))
+
+
+@skipUnless(scrypt, "Uninstalled scrypt module needed to generate hash")
+class TestLongTimeScryptHasher(TestCase):
+
+    def setUp(self):
+        scrypt_hashers = ("tests.test_subclassing.LongTimeScryptHasher",
+                          "django_scrypt.hashers.ScryptPasswordHasher") + default_hashers
+        load_hashers(password_hashers=scrypt_hashers)
+        self.password = 'letmein'
+
+    def test_LongTimeScryptHasher(self):
+        """Test operation of the extended time hasher that uses 16MB"""
+        encoded = make_password(self.password, hasher='scrypt_p')
+        self.assertTrue(check_password(self.password, encoded))
+
+    def test_LongTimeScryptHasher_takes_long_time(self):
+        """Test operation of the extended time hasher uses more time than default"""
+        start = time.clock()
+        encoded = make_password(self.password, hasher='scrypt')
+        self.assertTrue(check_password(self.password, encoded))
+        elapsed1 = (time.clock() - start)
+
+        start = time.clock()
+        encoded = make_password(self.password, hasher='scrypt_p')
+        self.assertTrue(check_password(self.password, encoded))
+        elapsed2 = (time.clock() - start)
+
+        self.assertTrue(elapsed1 < elapsed2)
+
+
+class LongTimeScryptHasher(ScryptPasswordHasher):
+    """This hasher is tuned for longer duration"""
+    algorithm = "scrypt_p"
+    p = 3
+
+
+class BigMemScryptHasher(ScryptPasswordHasher):
+    """This hasher is tuned to use lots of memory
+    (128 * 2 ** 14 * 18) ==  37748736 or ~36mb
+    """
+    algorithm = "scrypt_r"
+    r = 18
+
+
+class BigMemNScryptHasher(ScryptPasswordHasher):
+    """This hasher is tuned to use lots of memory
+    (128 * 2 ** 15 * 8) == 33554432 or ~32mb
+    """
+    algorithm = "scrypt_N"
+    N = 15
+[tox]
+envlist =
+    py26 , py27
+
+[testenv]
+commands =
+    python setup.py test
+deps =
+    django==1.4
+    scrypt==0.5.5