From 25f2acfed0fc110f88abbfffb5c5c62a76670db0 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 26 Mar 2013 11:44:26 -0400 Subject: Fixed #20138 -- Added BCryptSHA256PasswordHasher BCryptSHA256PasswordHasher pre-hashes the users password using SHA256 to prevent the 72 byte truncation inherient in the BCrypt algorithm. --- django/conf/global_settings.py | 1 + django/contrib/auth/hashers.py | 49 +++++++++++++++++++++++++++++++++--- django/contrib/auth/tests/hashers.py | 16 ++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) (limited to 'django') diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 42df4b601a..aa67e48bcd 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -515,6 +515,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 3 PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 480fde69ce..092cccedde 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import base64 +import binascii import hashlib from django.dispatch import receiver @@ -257,7 +258,7 @@ class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher): digest = hashlib.sha1 -class BCryptPasswordHasher(BasePasswordHasher): +class BCryptSHA256PasswordHasher(BasePasswordHasher): """ Secure password hashing using the bcrypt algorithm (recommended) @@ -266,7 +267,8 @@ class BCryptPasswordHasher(BasePasswordHasher): this library depends on native C code and might cause portability issues. """ - algorithm = "bcrypt" + algorithm = "bcrypt_sha256" + digest = hashlib.sha256 library = ("py-bcrypt", "bcrypt") rounds = 12 @@ -278,14 +280,34 @@ class BCryptPasswordHasher(BasePasswordHasher): bcrypt = self._load_library() # Need to reevaluate the force_bytes call once bcrypt is supported on # Python 3 - data = bcrypt.hashpw(force_bytes(password), salt) + + # Hash the password prior to using bcrypt to prevent password truncation + # See: https://code.djangoproject.com/ticket/20138 + if self.digest is not None: + # We use binascii.hexlify here because Python3 decided that a hex encoded + # bytestring is somehow a unicode. + password = binascii.hexlify(self.digest(force_bytes(password)).digest()) + else: + password = force_bytes(password) + + data = bcrypt.hashpw(password, salt) return "%s$%s" % (self.algorithm, data) def verify(self, password, encoded): algorithm, data = encoded.split('$', 1) assert algorithm == self.algorithm bcrypt = self._load_library() - return constant_time_compare(data, bcrypt.hashpw(force_bytes(password), data)) + + # Hash the password prior to using bcrypt to prevent password truncation + # See: https://code.djangoproject.com/ticket/20138 + if self.digest is not None: + # We use binascii.hexlify here because Python3 decided that a hex encoded + # bytestring is somehow a unicode. + password = binascii.hexlify(self.digest(force_bytes(password)).digest()) + else: + password = force_bytes(password) + + return constant_time_compare(data, bcrypt.hashpw(password, data)) def safe_summary(self, encoded): algorithm, empty, algostr, work_factor, data = encoded.split('$', 4) @@ -299,6 +321,25 @@ class BCryptPasswordHasher(BasePasswordHasher): ]) +class BCryptPasswordHasher(BCryptSHA256PasswordHasher): + """ + Secure password hashing using the bcrypt algorithm + + This is considered by many to be the most secure algorithm but you + must first install the py-bcrypt library. Please be warned that + this library depends on native C code and might cause portability + issues. + + This hasher does not first hash the password which means it is subject to + the 72 character bcrypt password truncation, most use cases should prefer + the BCryptSha512PasswordHasher. + + See: https://code.djangoproject.com/ticket/20138 + """ + algorithm = "bcrypt" + digest = None + + class SHA1PasswordHasher(BasePasswordHasher): """ The SHA1 password hashing algorithm (not recommended) diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py index 2b2243cb0c..9253fcbc43 100644 --- a/django/contrib/auth/tests/hashers.py +++ b/django/contrib/auth/tests/hashers.py @@ -92,6 +92,22 @@ class TestUtilsHashPass(unittest.TestCase): self.assertFalse(check_password('lètmeiz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "crypt") + @skipUnless(bcrypt, "py-bcrypt not installed") + def test_bcrypt_sha256(self): + encoded = make_password('lètmein', hasher='bcrypt_sha256') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(encoded.startswith('bcrypt_sha256$')) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt_sha256") + + # Verify that password truncation no longer works + password = ('VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5' + 'JGB3RGZ7VI7G7DJQ9NI8BQFSRPTG6UWTTVESA5ZPUN') + encoded = make_password(password, hasher='bcrypt_sha256') + self.assertTrue(check_password(password, encoded)) + self.assertFalse(check_password(password[:72], encoded)) + @skipUnless(bcrypt, "py-bcrypt not installed") def test_bcrypt(self): encoded = make_password('lètmein', hasher='bcrypt') -- cgit v1.3