Loading django/conf/global_settings.py +1 −0 Original line number Diff line number Diff line Loading @@ -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', Loading django/contrib/auth/hashers.py +45 −4 Original line number Diff line number Diff line from __future__ import unicode_literals import base64 import binascii import hashlib from django.dispatch import receiver Loading Loading @@ -257,7 +258,7 @@ class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher): digest = hashlib.sha1 class BCryptPasswordHasher(BasePasswordHasher): class BCryptSHA256PasswordHasher(BasePasswordHasher): """ Secure password hashing using the bcrypt algorithm (recommended) Loading @@ -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 Loading @@ -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) Loading @@ -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) Loading django/contrib/auth/tests/hashers.py +16 −0 Original line number Diff line number Diff line Loading @@ -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') Loading docs/releases/1.6.txt +3 −0 Original line number Diff line number Diff line Loading @@ -181,6 +181,9 @@ Minor features and the undocumented limit of the higher of 1000 or ``max_num`` forms was changed so it is always 1000 more than ``max_num``. * Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue with bcrypt. Backwards incompatible changes in 1.6 ===================================== Loading docs/topics/auth/passwords.txt +22 −3 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ The default for :setting:`PASSWORD_HASHERS` is:: 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', Loading Loading @@ -79,10 +80,11 @@ To use Bcrypt as your default storage algorithm, do the following: py-bcrypt``, or downloading the library and installing it with ``python setup.py install``). 2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptPasswordHasher`` 2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptSHA256PasswordHasher`` first. That is, in your settings file, you'd put:: PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', Loading @@ -97,6 +99,22 @@ To use Bcrypt as your default storage algorithm, do the following: That's it -- now your Django install will use Bcrypt as the default storage algorithm. .. admonition:: Password truncation with BCryptPasswordHasher The designers of bcrypt truncate all passwords at 72 characters which means that ``bcrypt(password_with_100_chars) == bcrypt(password_with_100_chars[:72])``. The original ``BCryptPasswordHasher`` does not have any special handling and thus is also subject to this hidden password length limit. ``BCryptSHA256PasswordHasher`` fixes this by first first hashing the password using sha256. This prevents the password truncation and so should be preferred over the ``BCryptPasswordHasher``. The practical ramification of this truncation is pretty marginal as the average user does not have a password greater than 72 characters in length and even being truncated at 72 the compute powered required to brute force bcrypt in any useful amount of time is still astronomical. Nonetheless, we recommend you use ``BCryptSHA256PasswordHasher`` anyway on the principle of "better safe than sorry. .. admonition:: Other bcrypt implementations There are several other implementations that allow bcrypt to be Loading Loading @@ -138,6 +156,7 @@ default PBKDF2 algorithm: 'myproject.hashers.MyPBKDF2PasswordHasher', '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', Loading Loading @@ -194,8 +213,8 @@ from the ``User`` model. provide a salt and a hashing algorithm to use, if you don't want to use the defaults (first entry of ``PASSWORD_HASHERS`` setting). Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``, ``'bcrypt'`` (see :ref:`bcrypt_usage`), ``'sha1'``, ``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` ``'bcrypt_sha256'`` (see :ref:`bcrypt_usage`), ``'bcrypt'``, ``'sha1'``, ``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` if you have the ``crypt`` library installed. If the password argument is ``None``, an unusable password is returned (a one that will be never accepted by :func:`check_password`). Loading Loading
django/conf/global_settings.py +1 −0 Original line number Diff line number Diff line Loading @@ -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', Loading
django/contrib/auth/hashers.py +45 −4 Original line number Diff line number Diff line from __future__ import unicode_literals import base64 import binascii import hashlib from django.dispatch import receiver Loading Loading @@ -257,7 +258,7 @@ class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher): digest = hashlib.sha1 class BCryptPasswordHasher(BasePasswordHasher): class BCryptSHA256PasswordHasher(BasePasswordHasher): """ Secure password hashing using the bcrypt algorithm (recommended) Loading @@ -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 Loading @@ -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) Loading @@ -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) Loading
django/contrib/auth/tests/hashers.py +16 −0 Original line number Diff line number Diff line Loading @@ -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') Loading
docs/releases/1.6.txt +3 −0 Original line number Diff line number Diff line Loading @@ -181,6 +181,9 @@ Minor features and the undocumented limit of the higher of 1000 or ``max_num`` forms was changed so it is always 1000 more than ``max_num``. * Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue with bcrypt. Backwards incompatible changes in 1.6 ===================================== Loading
docs/topics/auth/passwords.txt +22 −3 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ The default for :setting:`PASSWORD_HASHERS` is:: 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', Loading Loading @@ -79,10 +80,11 @@ To use Bcrypt as your default storage algorithm, do the following: py-bcrypt``, or downloading the library and installing it with ``python setup.py install``). 2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptPasswordHasher`` 2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptSHA256PasswordHasher`` first. That is, in your settings file, you'd put:: PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', Loading @@ -97,6 +99,22 @@ To use Bcrypt as your default storage algorithm, do the following: That's it -- now your Django install will use Bcrypt as the default storage algorithm. .. admonition:: Password truncation with BCryptPasswordHasher The designers of bcrypt truncate all passwords at 72 characters which means that ``bcrypt(password_with_100_chars) == bcrypt(password_with_100_chars[:72])``. The original ``BCryptPasswordHasher`` does not have any special handling and thus is also subject to this hidden password length limit. ``BCryptSHA256PasswordHasher`` fixes this by first first hashing the password using sha256. This prevents the password truncation and so should be preferred over the ``BCryptPasswordHasher``. The practical ramification of this truncation is pretty marginal as the average user does not have a password greater than 72 characters in length and even being truncated at 72 the compute powered required to brute force bcrypt in any useful amount of time is still astronomical. Nonetheless, we recommend you use ``BCryptSHA256PasswordHasher`` anyway on the principle of "better safe than sorry. .. admonition:: Other bcrypt implementations There are several other implementations that allow bcrypt to be Loading Loading @@ -138,6 +156,7 @@ default PBKDF2 algorithm: 'myproject.hashers.MyPBKDF2PasswordHasher', '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', Loading Loading @@ -194,8 +213,8 @@ from the ``User`` model. provide a salt and a hashing algorithm to use, if you don't want to use the defaults (first entry of ``PASSWORD_HASHERS`` setting). Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``, ``'bcrypt'`` (see :ref:`bcrypt_usage`), ``'sha1'``, ``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` ``'bcrypt_sha256'`` (see :ref:`bcrypt_usage`), ``'bcrypt'``, ``'sha1'``, ``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` if you have the ``crypt`` library installed. If the password argument is ``None``, an unusable password is returned (a one that will be never accepted by :func:`check_password`). Loading