Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge pull request #958 from dstufft/prehash-bcrypt

Fixed #20138 -- Added BCryptSHA256PasswordHasher
  • Loading branch information...
commit c1d4af6884fbf6c232f6d3fedd168609e0449239 2 parents e17fa9e + 7055915
Donald Stufft authored
1  AUTHORS
@@ -35,6 +35,7 @@ The PRIMARY AUTHORS are (and/or have been):
35 35
     * Bryan Veloso
36 36
     * Preston Holmes
37 37
     * Simon Charette
  38
+    * Donald Stufft
38 39
 
39 40
 More information on the main contributors to Django can be found in
40 41
 docs/internals/committers.txt.
1  django/conf/global_settings.py
@@ -515,6 +515,7 @@
515 515
 PASSWORD_HASHERS = (
516 516
     'django.contrib.auth.hashers.PBKDF2PasswordHasher',
517 517
     'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
  518
+    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
518 519
     'django.contrib.auth.hashers.BCryptPasswordHasher',
519 520
     'django.contrib.auth.hashers.SHA1PasswordHasher',
520 521
     'django.contrib.auth.hashers.MD5PasswordHasher',
49  django/contrib/auth/hashers.py
... ...
@@ -1,6 +1,7 @@
1 1
 from __future__ import unicode_literals
2 2
 
3 3
 import base64
  4
+import binascii
4 5
 import hashlib
5 6
 
6 7
 from django.dispatch import receiver
@@ -257,7 +258,7 @@ class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
257 258
     digest = hashlib.sha1
258 259
 
259 260
 
260  
-class BCryptPasswordHasher(BasePasswordHasher):
  261
+class BCryptSHA256PasswordHasher(BasePasswordHasher):
261 262
     """
262 263
     Secure password hashing using the bcrypt algorithm (recommended)
263 264
 
@@ -266,7 +267,8 @@ class BCryptPasswordHasher(BasePasswordHasher):
266 267
     this library depends on native C code and might cause portability
267 268
     issues.
268 269
     """
269  
-    algorithm = "bcrypt"
  270
+    algorithm = "bcrypt_sha256"
  271
+    digest = hashlib.sha256
270 272
     library = ("py-bcrypt", "bcrypt")
271 273
     rounds = 12
272 274
 
@@ -278,14 +280,34 @@ def encode(self, password, salt):
278 280
         bcrypt = self._load_library()
279 281
         # Need to reevaluate the force_bytes call once bcrypt is supported on
280 282
         # Python 3
281  
-        data = bcrypt.hashpw(force_bytes(password), salt)
  283
+
  284
+        # Hash the password prior to using bcrypt to prevent password truncation
  285
+        #   See: https://code.djangoproject.com/ticket/20138
  286
+        if self.digest is not None:
  287
+            # We use binascii.hexlify here because Python3 decided that a hex encoded
  288
+            #   bytestring is somehow a unicode.
  289
+            password = binascii.hexlify(self.digest(force_bytes(password)).digest())
  290
+        else:
  291
+            password = force_bytes(password)
  292
+
  293
+        data = bcrypt.hashpw(password, salt)
282 294
         return "%s$%s" % (self.algorithm, data)
283 295
 
284 296
     def verify(self, password, encoded):
285 297
         algorithm, data = encoded.split('$', 1)
286 298
         assert algorithm == self.algorithm
287 299
         bcrypt = self._load_library()
288  
-        return constant_time_compare(data, bcrypt.hashpw(force_bytes(password), data))
  300
+
  301
+        # Hash the password prior to using bcrypt to prevent password truncation
  302
+        #   See: https://code.djangoproject.com/ticket/20138
  303
+        if self.digest is not None:
  304
+            # We use binascii.hexlify here because Python3 decided that a hex encoded
  305
+            #   bytestring is somehow a unicode.
  306
+            password = binascii.hexlify(self.digest(force_bytes(password)).digest())
  307
+        else:
  308
+            password = force_bytes(password)
  309
+
  310
+        return constant_time_compare(data, bcrypt.hashpw(password, data))
289 311
 
290 312
     def safe_summary(self, encoded):
291 313
         algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
@@ -299,6 +321,25 @@ def safe_summary(self, encoded):
299 321
         ])
300 322
 
301 323
 
  324
+class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
  325
+    """
  326
+    Secure password hashing using the bcrypt algorithm
  327
+
  328
+    This is considered by many to be the most secure algorithm but you
  329
+    must first install the py-bcrypt library.  Please be warned that
  330
+    this library depends on native C code and might cause portability
  331
+    issues.
  332
+
  333
+    This hasher does not first hash the password which means it is subject to
  334
+    the 72 character bcrypt password truncation, most use cases should prefer
  335
+    the BCryptSha512PasswordHasher.
  336
+
  337
+    See: https://code.djangoproject.com/ticket/20138
  338
+    """
  339
+    algorithm = "bcrypt"
  340
+    digest = None
  341
+
  342
+
302 343
 class SHA1PasswordHasher(BasePasswordHasher):
303 344
     """
304 345
     The SHA1 password hashing algorithm (not recommended)
16  django/contrib/auth/tests/hashers.py
@@ -93,6 +93,22 @@ def test_crypt(self):
93 93
         self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
94 94
 
95 95
     @skipUnless(bcrypt, "py-bcrypt not installed")
  96
+    def test_bcrypt_sha256(self):
  97
+        encoded = make_password('lètmein', hasher='bcrypt_sha256')
  98
+        self.assertTrue(is_password_usable(encoded))
  99
+        self.assertTrue(encoded.startswith('bcrypt_sha256$'))
  100
+        self.assertTrue(check_password('lètmein', encoded))
  101
+        self.assertFalse(check_password('lètmeinz', encoded))
  102
+        self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt_sha256")
  103
+
  104
+        # Verify that password truncation no longer works
  105
+        password = ('VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5'
  106
+                    'JGB3RGZ7VI7G7DJQ9NI8BQFSRPTG6UWTTVESA5ZPUN')
  107
+        encoded = make_password(password, hasher='bcrypt_sha256')
  108
+        self.assertTrue(check_password(password, encoded))
  109
+        self.assertFalse(check_password(password[:72], encoded))
  110
+
  111
+    @skipUnless(bcrypt, "py-bcrypt not installed")
96 112
     def test_bcrypt(self):
97 113
         encoded = make_password('lètmein', hasher='bcrypt')
98 114
         self.assertTrue(is_password_usable(encoded))
3  docs/releases/1.6.txt
@@ -181,6 +181,9 @@ Minor features
181 181
   and the undocumented limit of the higher of 1000 or ``max_num`` forms
182 182
   was changed so it is always 1000 more than ``max_num``.
183 183
 
  184
+* Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue
  185
+  with bcrypt.
  186
+
184 187
 Backwards incompatible changes in 1.6
185 188
 =====================================
186 189
 
25  docs/topics/auth/passwords.txt
@@ -52,6 +52,7 @@ The default for :setting:`PASSWORD_HASHERS` is::
52 52
     PASSWORD_HASHERS = (
53 53
         'django.contrib.auth.hashers.PBKDF2PasswordHasher',
54 54
         'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
  55
+        'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
55 56
         'django.contrib.auth.hashers.BCryptPasswordHasher',
56 57
         'django.contrib.auth.hashers.SHA1PasswordHasher',
57 58
         'django.contrib.auth.hashers.MD5PasswordHasher',
@@ -79,10 +80,11 @@ To use Bcrypt as your default storage algorithm, do the following:
79 80
    py-bcrypt``, or downloading the library and installing it with ``python
80 81
    setup.py install``).
81 82
 
82  
-2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptPasswordHasher``
  83
+2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptSHA256PasswordHasher``
83 84
    first. That is, in your settings file, you'd put::
84 85
 
85 86
         PASSWORD_HASHERS = (
  87
+            'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
86 88
             'django.contrib.auth.hashers.BCryptPasswordHasher',
87 89
             'django.contrib.auth.hashers.PBKDF2PasswordHasher',
88 90
             'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
@@ -97,6 +99,22 @@ To use Bcrypt as your default storage algorithm, do the following:
97 99
 That's it -- now your Django install will use Bcrypt as the default storage
98 100
 algorithm.
99 101
 
  102
+.. admonition:: Password truncation with BCryptPasswordHasher
  103
+
  104
+    The designers of bcrypt truncate all passwords at 72 characters which means
  105
+    that ``bcrypt(password_with_100_chars) == bcrypt(password_with_100_chars[:72])``.
  106
+    The original ``BCryptPasswordHasher`` does not have any special handling and
  107
+    thus is also subject to this hidden password length limit.
  108
+    ``BCryptSHA256PasswordHasher`` fixes this by first first hashing the
  109
+    password using sha256. This prevents the password truncation and so should
  110
+    be preferred over the ``BCryptPasswordHasher``. The practical ramification
  111
+    of this truncation is pretty marginal as the average user does not have a
  112
+    password greater than 72 characters in length and even being truncated at 72
  113
+    the compute powered required to brute force bcrypt in any useful amount of
  114
+    time is still astronomical. Nonetheless, we recommend you use
  115
+    ``BCryptSHA256PasswordHasher`` anyway on the principle of "better safe than
  116
+    sorry.
  117
+
100 118
 .. admonition:: Other bcrypt implementations
101 119
 
102 120
    There are several other implementations that allow bcrypt to be
@@ -138,6 +156,7 @@ default PBKDF2 algorithm:
138 156
             'myproject.hashers.MyPBKDF2PasswordHasher',
139 157
             'django.contrib.auth.hashers.PBKDF2PasswordHasher',
140 158
             'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
  159
+            'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
141 160
             'django.contrib.auth.hashers.BCryptPasswordHasher',
142 161
             'django.contrib.auth.hashers.SHA1PasswordHasher',
143 162
             'django.contrib.auth.hashers.MD5PasswordHasher',
@@ -194,8 +213,8 @@ from the ``User`` model.
194 213
     provide a salt and a hashing algorithm to use, if you don't want to use the
195 214
     defaults (first entry of ``PASSWORD_HASHERS`` setting).
196 215
     Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``,
197  
-    ``'bcrypt'`` (see :ref:`bcrypt_usage`), ``'sha1'``, ``'md5'``,
198  
-    ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'``
  216
+    ``'bcrypt_sha256'`` (see :ref:`bcrypt_usage`), ``'bcrypt'``, ``'sha1'``,
  217
+    ``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'``
199 218
     if you have the ``crypt`` library installed. If the password argument is
200 219
     ``None``, an unusable password is returned (a one that will be never
201 220
     accepted by :func:`check_password`).

0 notes on commit c1d4af6

Please sign in to comment.
Something went wrong with that request. Please try again.