Commit c1d4af68 authored by Donald Stufft's avatar Donald Stufft
Browse files

Merge pull request #958 from dstufft/prehash-bcrypt

Fixed #20138 -- Added BCryptSHA256PasswordHasher
parents e17fa9e8 70559150
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ The PRIMARY AUTHORS are (and/or have been):
    * Bryan Veloso
    * Preston Holmes
    * Simon Charette
    * Donald Stufft

More information on the main contributors to Django can be found in
docs/internals/committers.txt.
+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
=====================================

Loading