Fixed #20138 -- Added BCryptSHA256PasswordHasher
BCryptSHA256PasswordHasher pre-hashes the users password using SHA256 to prevent the 72 byte truncation inherient in the BCrypt algorithm.
This commit is contained in:
parent
e17fa9e877
commit
25f2acfed0
|
@ -515,6 +515,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 3
|
||||||
PASSWORD_HASHERS = (
|
PASSWORD_HASHERS = (
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -257,7 +258,7 @@ class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
|
||||||
digest = hashlib.sha1
|
digest = hashlib.sha1
|
||||||
|
|
||||||
|
|
||||||
class BCryptPasswordHasher(BasePasswordHasher):
|
class BCryptSHA256PasswordHasher(BasePasswordHasher):
|
||||||
"""
|
"""
|
||||||
Secure password hashing using the bcrypt algorithm (recommended)
|
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
|
this library depends on native C code and might cause portability
|
||||||
issues.
|
issues.
|
||||||
"""
|
"""
|
||||||
algorithm = "bcrypt"
|
algorithm = "bcrypt_sha256"
|
||||||
|
digest = hashlib.sha256
|
||||||
library = ("py-bcrypt", "bcrypt")
|
library = ("py-bcrypt", "bcrypt")
|
||||||
rounds = 12
|
rounds = 12
|
||||||
|
|
||||||
|
@ -278,14 +280,34 @@ class BCryptPasswordHasher(BasePasswordHasher):
|
||||||
bcrypt = self._load_library()
|
bcrypt = self._load_library()
|
||||||
# Need to reevaluate the force_bytes call once bcrypt is supported on
|
# Need to reevaluate the force_bytes call once bcrypt is supported on
|
||||||
# Python 3
|
# 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)
|
return "%s$%s" % (self.algorithm, data)
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
algorithm, data = encoded.split('$', 1)
|
algorithm, data = encoded.split('$', 1)
|
||||||
assert algorithm == self.algorithm
|
assert algorithm == self.algorithm
|
||||||
bcrypt = self._load_library()
|
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):
|
def safe_summary(self, encoded):
|
||||||
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
|
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):
|
class SHA1PasswordHasher(BasePasswordHasher):
|
||||||
"""
|
"""
|
||||||
The SHA1 password hashing algorithm (not recommended)
|
The SHA1 password hashing algorithm (not recommended)
|
||||||
|
|
|
@ -92,6 +92,22 @@ class TestUtilsHashPass(unittest.TestCase):
|
||||||
self.assertFalse(check_password('lètmeiz', encoded))
|
self.assertFalse(check_password('lètmeiz', encoded))
|
||||||
self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
|
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")
|
@skipUnless(bcrypt, "py-bcrypt not installed")
|
||||||
def test_bcrypt(self):
|
def test_bcrypt(self):
|
||||||
encoded = make_password('lètmein', hasher='bcrypt')
|
encoded = make_password('lètmein', hasher='bcrypt')
|
||||||
|
|
|
@ -181,6 +181,9 @@ Minor features
|
||||||
and the undocumented limit of the higher of 1000 or ``max_num`` forms
|
and the undocumented limit of the higher of 1000 or ``max_num`` forms
|
||||||
was changed so it is always 1000 more than ``max_num``.
|
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
|
Backwards incompatible changes in 1.6
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ The default for :setting:`PASSWORD_HASHERS` is::
|
||||||
PASSWORD_HASHERS = (
|
PASSWORD_HASHERS = (
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
@ -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
|
py-bcrypt``, or downloading the library and installing it with ``python
|
||||||
setup.py install``).
|
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::
|
first. That is, in your settings file, you'd put::
|
||||||
|
|
||||||
PASSWORD_HASHERS = (
|
PASSWORD_HASHERS = (
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
@ -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
|
That's it -- now your Django install will use Bcrypt as the default storage
|
||||||
algorithm.
|
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
|
.. admonition:: Other bcrypt implementations
|
||||||
|
|
||||||
There are several other implementations that allow bcrypt to be
|
There are several other implementations that allow bcrypt to be
|
||||||
|
@ -138,6 +156,7 @@ default PBKDF2 algorithm:
|
||||||
'myproject.hashers.MyPBKDF2PasswordHasher',
|
'myproject.hashers.MyPBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
@ -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
|
provide a salt and a hashing algorithm to use, if you don't want to use the
|
||||||
defaults (first entry of ``PASSWORD_HASHERS`` setting).
|
defaults (first entry of ``PASSWORD_HASHERS`` setting).
|
||||||
Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``,
|
Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``,
|
||||||
``'bcrypt'`` (see :ref:`bcrypt_usage`), ``'sha1'``, ``'md5'``,
|
``'bcrypt_sha256'`` (see :ref:`bcrypt_usage`), ``'bcrypt'``, ``'sha1'``,
|
||||||
``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'``
|
``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'``
|
||||||
if you have the ``crypt`` library installed. If the password argument is
|
if you have the ``crypt`` library installed. If the password argument is
|
||||||
``None``, an unusable password is returned (a one that will be never
|
``None``, an unusable password is returned (a one that will be never
|
||||||
accepted by :func:`check_password`).
|
accepted by :func:`check_password`).
|
||||||
|
|
Loading…
Reference in New Issue