Description: CVE-2016-2513: Fixed user enumeration timing attack during login
Origin: backport, https://github.com/django/django/commit/f4e6e02f7713a6924d16540be279909ff4091eb6, https://github.com/django/django/commit/7d0d0dbf26a3c0d16e9c2b930fd6d7b89f215946
Forwarded: not-needed
Author: Florian Apolloner <florian@apolloner.eu>
Reviewed-by: Salvatore Bonaccorso <carnil@debian.org>
Last-Update: 2016-03-12
Applied-Upstream: 1.8.10

---
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -1,10 +1,11 @@
 import functools
 import hashlib
+import warnings
 
 from django.conf import settings
 from django.utils import importlib
 from django.utils.datastructures import SortedDict
-from django.utils.encoding import smart_str
+from django.utils.encoding import force_bytes, smart_str
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.crypto import (
     pbkdf2, constant_time_compare, get_random_string)
@@ -56,8 +57,17 @@ def check_password(password, encoded, se
         algorithm = encoded.split('$', 1)[0]
         hasher = get_hasher(algorithm)
 
-    must_update = hasher.algorithm != preferred.algorithm
+    hasher_changed = hasher.algorithm != preferred.algorithm
+    must_update = hasher_changed or preferred.must_update(encoded)
     is_correct = hasher.verify(password, encoded)
+
+    # If the hasher didn't change (we don't protect against enumeration if it
+    # does) and the password should get updated, try to close the timing gap
+    # between the work factor of the current encoded password and the default
+    # work factor.
+    if not is_correct and not hasher_changed and must_update:
+        hasher.harden_runtime(password, encoded)
+
     if setter and is_correct and must_update:
         setter(raw_password)
     return is_correct
@@ -198,6 +208,22 @@ class BasePasswordHasher(object):
         """
         raise NotImplementedError()
 
+    def must_update(self, encoded):
+        return False
+
+    def harden_runtime(self, password, encoded):
+        """
+        Bridge the runtime gap between the work factor supplied in `encoded`
+        and the work factor suggested by this hasher.
+
+        Taking PBKDF2 as an example, if `encoded` contains 20000 iterations and
+        `self.iterations` is 30000, this method should run password through
+        another 10000 iterations of PBKDF2. Similar approaches should exist
+        for any hasher that has a work factor. If not, this method should be
+        defined as a no-op to silence the warning.
+        """
+        warnings.warn('subclasses of BasePasswordHasher should provide a harden_runtime() method')
+
 
 class PBKDF2PasswordHasher(BasePasswordHasher):
     """
@@ -238,6 +264,16 @@ class PBKDF2PasswordHasher(BasePasswordH
             (_('hash'), mask_hash(hash)),
         ])
 
+    def must_update(self, encoded):
+        algorithm, iterations, salt, hash = encoded.split('$', 3)
+        return int(iterations) != self.iterations
+
+    def harden_runtime(self, password, encoded):
+        algorithm, iterations, salt, hash = encoded.split('$', 3)
+        extra_iterations = self.iterations - int(iterations)
+        if extra_iterations > 0:
+            self.encode(password, salt, extra_iterations)
+
 
 class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
     """
@@ -291,6 +327,16 @@ class BCryptPasswordHasher(BasePasswordH
             (_('checksum'), mask_hash(checksum)),
         ])
 
+    def harden_runtime(self, password, encoded):
+        _, data = encoded.split('$', 1)
+        salt = data[:29]  # Length of the salt in bcrypt.
+        rounds = data.split('$')[2]
+        # work factor is logarithmic, adding one doubles the load.
+        diff = 2**(self.rounds - int(rounds)) - 1
+        while diff > 0:
+            self.encode(password, force_bytes(salt))
+            diff -= 1
+
 
 class SHA1PasswordHasher(BasePasswordHasher):
     """
@@ -321,6 +367,10 @@ class SHA1PasswordHasher(BasePasswordHas
             (_('hash'), mask_hash(hash)),
         ])
 
+    def harden_runtime(self, password, encoded):
+        pass
+
+
 
 class MD5PasswordHasher(BasePasswordHasher):
     """
@@ -351,6 +401,10 @@ class MD5PasswordHasher(BasePasswordHash
             (_('hash'), mask_hash(hash)),
         ])
 
+    def harden_runtime(self, password, encoded):
+        pass
+
+
 
 class UnsaltedMD5PasswordHasher(BasePasswordHasher):
     """
@@ -384,6 +438,10 @@ class UnsaltedMD5PasswordHasher(BasePass
             (_('hash'), mask_hash(encoded, show=3)),
         ])
 
+    def harden_runtime(self, password, encoded):
+        pass
+
+
 
 class CryptPasswordHasher(BasePasswordHasher):
     """
@@ -420,3 +478,6 @@ class CryptPasswordHasher(BasePasswordHa
             (_('salt'), salt),
             (_('hash'), mask_hash(data, show=3)),
         ])
+
+    def harden_runtime(self, password, encoded):
+        pass
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -527,12 +527,42 @@ However, Django can only upgrade passwor
 sure never to *remove* entries from this list. If you do, users using un-
 mentioned algorithms won't be able to upgrade.
 
+Be aware that if all the passwords in your database aren't encoded in the
+default hasher's algorithm, you may be vulnerable to a user enumeration timing
+attack due to a difference between the duration of a login request for a user
+with a password encoded in a non-default algorithm and the duration of a login
+request for a nonexistent user (which runs the default hasher). You may be able
+to mitigate this by upgrading older password hashes.
+
 .. _sha1: http://en.wikipedia.org/wiki/SHA1
 .. _pbkdf2: http://en.wikipedia.org/wiki/PBKDF2
 .. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
 .. _bcrypt: http://en.wikipedia.org/wiki/Bcrypt
 .. _py-bcrypt: http://pypi.python.org/pypi/py-bcrypt/
 
+.. _write-your-own-password-hasher:
+
+Writing your own hasher
+-----------------------
+
+.. versionadded:: 1.8.10
+
+If you write your own password hasher that contains a work factor such as a
+number of iterations, you should implement a
+``harden_runtime(self, password, encoded)`` method to bridge the runtime gap
+between the work factor supplied in the ``encoded`` password and the default
+work factor of the hasher. This prevents a user enumeration timing attack due
+to  difference between a login request for a user with a password encoded in an
+older number of iterations and a nonexistent user (which runs the default
+hasher's default number of iterations).
+
+Taking PBKDF2 as example, if ``encoded`` contains 20,000 iterations and the
+hasher's default ``iterations`` is 30,000, the method should run ``password``
+through another 10,000 iterations of PBKDF2.
+
+If your hasher doesn't have a work factor, implement the method as a no-op
+(``pass``).
+
 Anonymous users
 ---------------
 
--- a/django/utils/encoding.py
+++ b/django/utils/encoding.py
@@ -51,6 +51,42 @@ def is_protected_type(obj):
         float, Decimal)
     )
 
+def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'):
+    """
+    Similar to smart_bytes, except that lazy instances are resolved to
+    strings, rather than kept as lazy objects.
+
+    If strings_only is True, don't convert (some) non-string-like objects.
+    """
+    # Handle the common case first for performance reasons.
+    if isinstance(s, bytes):
+        if encoding == 'utf-8':
+            return s
+        else:
+            return s.decode('utf-8', errors).encode(encoding, errors)
+    if strings_only and is_protected_type(s):
+        return s
+    if isinstance(s, six.memoryview):
+        return bytes(s)
+    if isinstance(s, Promise):
+        return six.text_type(s).encode(encoding, errors)
+    if not isinstance(s, six.string_types):
+        try:
+            if six.PY3:
+                return six.text_type(s).encode(encoding)
+            else:
+                return bytes(s)
+        except UnicodeEncodeError:
+            if isinstance(s, Exception):
+                # An Exception subclass containing non-ASCII data that doesn't
+                # know how to print itself properly. We shouldn't raise a
+                # further exception.
+                return b' '.join([force_bytes(arg, encoding, strings_only,
+                        errors) for arg in s])
+            return six.text_type(s).encode(encoding, errors)
+    else:
+        return s.encode(encoding, errors)
+
 def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'):
     """
     Similar to smart_unicode, except that lazy instances are resolved to
