Commit 25f2acfe authored by Donald Stufft's avatar Donald Stufft
Browse files

Fixed #20138 -- Added BCryptSHA256PasswordHasher

BCryptSHA256PasswordHasher pre-hashes the users password using
SHA256 to prevent the 72 byte truncation inherient in the BCrypt
algorithm.
parent e17fa9e8
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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',
+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
@@ -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)
+16 −0
Original line number Diff line number Diff line
@@ -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')
+3 −0
Original line number Diff line number Diff line
@@ -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
=====================================

+22 −3
Original line number Diff line number Diff line
@@ -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',
@@ -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',
@@ -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
@@ -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',
@@ -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`).