Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Fixed #20138 -- Added BCryptSHA256PasswordHasher #958

Merged
merged 2 commits into from

5 participants

@dstufft
Collaborator

BCryptSHA256PasswordHasher pre-hashes the users password using SHA256 to prevent the 72 byte truncation inherient in the BCrypt algorithm.

docs/topics/auth/passwords.txt
@@ -97,6 +99,16 @@ 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])``.
@alex Collaborator
alex added a note

Stray before )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
docs/topics/auth/passwords.txt
@@ -97,6 +99,16 @@ 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 did not have any special handling and thus
+ is also subject to a hidden effective password length limit. To combat this
+ a new password hasher (BCryptSHA256PasswordHasher) has been created which
@alex Collaborator
alex added a note

Use class reference reST syntax here (and above)

@dstufft Collaborator
dstufft added a note

I don't think I can use the reference syntax because the hashers themselves are not documented (and thus the reference has nothing to link too).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@alex
Collaborator

At least stick it in ``, so that it shows up in the code formatting.

docs/topics/auth/passwords.txt
@@ -97,6 +99,16 @@ 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 did not have any special handling and thus
+ is also subject to a hidden effective password length limit. To combat this
+ a new password hasher (BCryptSHA256PasswordHasher) has been created which
@mjtamlyn Collaborator

"new" only makes sense for a while...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dstufft
Collaborator

Addred `` to make class named show up in code formatting, removed the "new" from the admonition, and added a section to the release notes.

@mjtamlyn
Collaborator

Would the intention be to remove or deprecate the old hasher at some point? It seems like a fairly significant flaw with it. Alternatively perhaps it should have a check that passwords are <72 characters?

@dstufft
Collaborator

We cannot remove the old hasher because otherwise passwords that are hashed with it cannot be automatically upgraded to a better hash. So it will stick around and just be not recommended like the other hashes. The practicality of this flaw is pretty small since even brute forcing the first 72 characters of a password hashed with bcrypt would take an astronomical amount of time.

@dstufft
Collaborator

Related Pull Requests:

  • Documentation for 1.5 #961
  • Documentation for 1.4 #962
docs/topics/auth/passwords.txt
@@ -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`` did not have any special handling and
+ thus is also subject to a hidden effective password length limit. To combat
+ this a password hasher (``BCryptSHA256PasswordHasher``) has been created
+ which first hashes the password using sha256. This prevents the password
+ truncation and should be preferred over the original ``BCryptPasswordHasher``.
@jacobian Collaborator

Minor edit:

``BCryptPasswordHasher`` does not have any special handling and
thus is also subject to this hidden password length limit. To combat
``BCryptSHA256PasswordHasher`` fixes this by first first hashing
the password using sha256. This prevents the password truncation
and so should be preferred over the ``BCryptPasswordHasher``.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
dstufft added some commits
@dstufft dstufft Fixed #20138 -- Added BCryptSHA256PasswordHasher
BCryptSHA256PasswordHasher pre-hashes the users password using
SHA256 to prevent the 72 byte truncation inherient in the BCrypt
algorithm.
25f2acf
@dstufft dstufft Add Donald Stufft to the AUTHORS file 7055915
@dstufft dstufft merged commit c1d4af6 into from
@dstufft dstufft deleted the branch
@uruz

Typo: forgotten quote

Collaborator

Fixed here f2a0be6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 26, 2013
  1. @dstufft

    Fixed #20138 -- Added BCryptSHA256PasswordHasher

    dstufft authored
    BCryptSHA256PasswordHasher pre-hashes the users password using
    SHA256 to prevent the 72 byte truncation inherient in the BCrypt
    algorithm.
  2. @dstufft
This page is out of date. Refresh to see the latest.
View
1  AUTHORS
@@ -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.
View
1  django/conf/global_settings.py
@@ -515,6 +515,7 @@
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',
View
49 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 @@ def encode(self, password, salt):
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 @@ def safe_summary(self, encoded):
])
+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)
View
16 django/contrib/auth/tests/hashers.py
@@ -93,6 +93,22 @@ def test_crypt(self):
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')
self.assertTrue(is_password_usable(encoded))
View
3  docs/releases/1.6.txt
@@ -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
=====================================
View
25 docs/topics/auth/passwords.txt
@@ -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`).
Something went wrong with that request. Please try again.