Commits

Kelvin Wong committed 471918d

Test for stored encoded hashes old and new formats

  • Participants
  • Parent commits bf3b169

Comments (0)

Files changed (3)

django_scrypt/__init__.py

 'django.contrib.auth.hashers.CryptPasswordHasher',
 )
 """
-__version__ = '0.2.1'
-__all__ = ['hashers']
+__version__ = '0.2.2'
+__all__ = ['hashers']

django_scrypt/hashers.py

-"""Contains the class used to create and store Scrypt message digests
+"""Create and store Scrypt message digests
 """
 from django.contrib.auth.hashers import BasePasswordHasher, mask_hash
 from django.utils.datastructures import SortedDict
 
 class ScryptPasswordHasher(BasePasswordHasher):
     """
-    Secure password hashing using the scrypt algorithm
+    A secure password hasher using the Scrypt algorithm
 
-    The py-scrypt library must be installed separately. That library
+    This subclass overrides the 'verify', 'encode', and 'safe_summary'
+    methods of BasePasswordHasher to allow Django to use the Scrypt
+    memory-hard key derivation function.
+
+    Subclass to modify parameters for custom Scrypt tuning.
+
+    The Py-Scrypt library must be installed separately. That library
     depends on native C code and might cause portability issues.
+
+    Class Attributes
+
+    algorithm -- Unique algorithm identifier used in encoded digests
+    library -- Import name of the required Py-Scrypt library
+    Nexp -- Default exponent value used to calculate N = 2 ** Nexp
+    r -- Default r-value used by Scrypt as positive integer
+    p -- Default p-value used by Scrypt as positive integer
+    buflen -- Unimplemented, holds byte length of the message digest
+
     """
 
     algorithm = "scrypt"
     def verify(self, password, encoded):
         """
         Checks if the given password is correct
+
+        password -- Password to be verified
+        encoded -- An encoded Scrypt message digest for comparison
+
+        Returns boolean True or False
+
         """
         algorithm, salt, Nexp, r, p, buflen, h = encoded.split('$')
         assert algorithm == self.algorithm
         Creates an encoded hash string from password, salt and optional
         parameters.
 
-        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
+        When used with a custom subclass, this method may return strings
+        longer than 128 characters (Django 1.4 password length limit)
+
+        password -- User's chosen password
+        salt -- Random string, 12-characters [a-zA-Z0-9] by default
+        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 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)
+        r -- The r-value passed to Scrypt
+        p -- The p-value passed to Scrypt
+        buflen -- Length of the returned hash in bytes (not implemented)
 
         Returns "scrypt$salt$Nexp$r$p$buflen$hash" where hash is a base64
-        encoded byte string (64-bytes by default)
+        encoded byte string (64-bytes or 512-bits by default)
+
         """
         assert password
         assert salt and '$' not in salt
         """
         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.
+        encoded -- An encoded hash (see encode method for format)
+
+        Returns a dictionary (SortedDict) used when password info
+        must be displayed
+
         """
         algorithm, salt, Nexp, r, p, buflen, h = encoded.split('$')
         assert algorithm == self.algorithm

tests/test_django_scrypt.py

         self.password = 'letmein'
         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_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_hash_less_than_128_characters(self):
+    def test_default_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)
         self.assertTrue(d[_('buflen')].isdigit())
         self.assertTrue(len(d[_('hash')]))
 
+    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_verify_default_hash_format_usable(self):
+        """Test encoded format passes good password"""
+        self.assertTrue(check_password(self.password, self.encoded_hash))
+
+    def test_verify_old_hash_format_raises_error(self):
+        """Test that old encoded format raises error"""
+        with self.assertRaises(Exception) as cm:
+            check_password(self.password, self.old_format_encoded_hash)
+        self.assertEqual(
+            "hash parameters are wrong (r*p should be < 2**30, and N should be a power of two > 1)",
+            str(cm.exception))
+
+    def test_verify_old_hash_format_fixable(self):
+        """Test deprecated encoded format can be fixed by swapping Nexp for N
+        
+        Specifically, replace 16384 with 14 at position 3 of the encoded hash
+        """
+        self.assertTrue(check_password(self.password, self.old_format_fix_encoded_hash))
+
     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)