diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 4f0f8337eb..806892d3ca 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -498,6 +498,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
# The number of days a password reset link is valid for
PASSWORD_RESET_TIMEOUT_DAYS = 3
+# the first hasher in this list is the preferred algorithm. any
+# password using different algorithms will be converted automatically
+# upon login
+PASSWORD_HASHERS = (
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+)
+
###########
# SIGNING #
###########
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 57ea108eb4..50b2e2b757 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,13 +1,14 @@
from django import forms
from django.forms.util import flatatt
from django.template import loader
+from django.utils.encoding import smart_str
from django.utils.http import int_to_base36
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import User
-from django.contrib.auth.utils import UNUSABLE_PASSWORD
from django.contrib.auth import authenticate
+from django.contrib.auth.models import User
+from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site
@@ -18,27 +19,26 @@ mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p
class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs):
- if not value:
+ encoded = value
+
+ if not is_password_usable(encoded):
return "None"
+
final_attrs = self.build_attrs(attrs)
- parts = value.split("$")
- if len(parts) != 3:
- # Legacy passwords didn't specify a hash type and were md5.
- hash_type = "md5"
- masked = mask_password(value)
+
+ encoded = smart_str(encoded)
+
+ if len(encoded) == 32 and '$' not in encoded:
+ hasher = get_hasher('md5')
else:
- hash_type = parts[0]
- masked = mask_password(parts[2])
- return mark_safe("""
- %(hash_type_label)s: %(hash_type)s
- %(masked_label)s: %(masked)s
-
""" % {
- "attrs": flatatt(final_attrs),
- "hash_type_label": _("Hash type"),
- "hash_type": hash_type,
- "masked_label": _("Masked hash"),
- "masked": masked,
- })
+ algorithm = encoded.split('$', 1)[0]
+ hasher = get_hasher(algorithm)
+
+ summary = ""
+ for key, value in hasher.safe_summary(encoded).iteritems():
+ summary += "%(key)s: %(value)s " % {"key": key, "value": value}
+
+ return mark_safe("%(summary)s
" % {"attrs": flatatt(final_attrs), "summary": summary})
class ReadOnlyPasswordHashField(forms.Field):
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 8ac94c1e8a..36ac6d8605 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -9,11 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.contrib import auth
-from django.contrib.auth.signals import user_logged_in
# UNUSABLE_PASSWORD is still imported here for backwards compatibility
-from django.contrib.auth.utils import (get_hexdigest, make_password,
- check_password, is_password_usable, get_random_string,
- UNUSABLE_PASSWORD)
+from django.contrib.auth.hashers import (
+ check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
+from django.contrib.auth.signals import user_logged_in
from django.contrib.contenttypes.models import ContentType
def update_last_login(sender, user, **kwargs):
@@ -220,27 +219,21 @@ class User(models.Model):
return full_name.strip()
def set_password(self, raw_password):
- self.password = make_password('sha1', raw_password)
+ self.password = make_password(raw_password)
def check_password(self, raw_password):
"""
Returns a boolean of whether the raw_password was correct. Handles
hashing formats behind the scenes.
"""
- # Backwards-compatibility check. Older passwords won't include the
- # algorithm or salt.
- if '$' not in self.password:
- is_correct = (self.password == get_hexdigest('md5', '', raw_password))
- if is_correct:
- # Convert the password to the new, more secure format.
- self.set_password(raw_password)
- self.save()
- return is_correct
- return check_password(raw_password, self.password)
+ def setter(raw_password):
+ self.set_password(raw_password)
+ self.save()
+ return check_password(raw_password, self.password, setter)
def set_unusable_password(self):
# Sets a value that will never be a valid hash
- self.password = make_password('sha1', None)
+ self.password = make_password(None)
def has_usable_password(self):
return is_password_usable(self.password)
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
index 7cb0dcb733..883e4c944b 100644
--- a/django/contrib/auth/tests/__init__.py
+++ b/django/contrib/auth/tests/__init__.py
@@ -1,7 +1,7 @@
from django.contrib.auth.tests.auth_backends import (BackendTest,
RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest,
InActiveUserBackendTest, NoInActiveUserBackendTest)
-from django.contrib.auth.tests.basic import BasicTestCase, PasswordUtilsTestCase
+from django.contrib.auth.tests.basic import BasicTestCase
from django.contrib.auth.tests.context_processors import AuthContextProcessorTests
from django.contrib.auth.tests.decorators import LoginRequiredTestCase
from django.contrib.auth.tests.forms import (UserCreationFormTest,
@@ -11,9 +11,11 @@ from django.contrib.auth.tests.remote_user import (RemoteUserTest,
RemoteUserNoCreateTest, RemoteUserCustomTest)
from django.contrib.auth.tests.management import GetDefaultUsernameTestCase
from django.contrib.auth.tests.models import ProfileTestCase
+from django.contrib.auth.tests.hashers import TestUtilsHashPass
from django.contrib.auth.tests.signals import SignalTestCase
from django.contrib.auth.tests.tokens import TokenGeneratorTest
-from django.contrib.auth.tests.views import (AuthViewNamedURLTests, PasswordResetTest,
- ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings)
+from django.contrib.auth.tests.views import (AuthViewNamedURLTests,
+ PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest,
+ LoginURLSettings)
# The password for the fixture data users is 'password'
diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py
index 9f94c2a769..512de163d9 100644
--- a/django/contrib/auth/tests/basic.py
+++ b/django/contrib/auth/tests/basic.py
@@ -1,7 +1,6 @@
from django.test import TestCase
from django.utils.unittest import skipUnless
from django.contrib.auth.models import User, AnonymousUser
-from django.contrib.auth import utils
from django.core.management import call_command
from StringIO import StringIO
@@ -111,30 +110,3 @@ class BasicTestCase(TestCase):
u = User.objects.get(username="joe+admin@somewhere.org")
self.assertEqual(u.email, 'joe@somewhere.org')
self.assertFalse(u.has_usable_password())
-
-
-class PasswordUtilsTestCase(TestCase):
-
- def _test_make_password(self, algo):
- password = utils.make_password(algo, "foobar")
- self.assertTrue(utils.is_password_usable(password))
- self.assertTrue(utils.check_password("foobar", password))
-
- def test_make_unusable(self):
- "Check that you can create an unusable password."
- password = utils.make_password("any", None)
- self.assertFalse(utils.is_password_usable(password))
- self.assertFalse(utils.check_password("foobar", password))
-
- def test_make_password_sha1(self):
- "Check creating passwords with SHA1 algorithm."
- self._test_make_password("sha1")
-
- def test_make_password_md5(self):
- "Check creating passwords with MD5 algorithm."
- self._test_make_password("md5")
-
- @skipUnless(crypt_module, "no crypt module to generate password.")
- def test_make_password_crypt(self):
- "Check creating passwords with CRYPT algorithm."
- self._test_make_password("crypt")
diff --git a/django/utils/crypto.py b/django/utils/crypto.py
index 95af6808fa..ff6096c6f9 100644
--- a/django/utils/crypto.py
+++ b/django/utils/crypto.py
@@ -2,10 +2,18 @@
Django's standard crypto functions and utilities.
"""
-import hashlib
import hmac
+import struct
+import hashlib
+import binascii
+import operator
from django.conf import settings
+
+trans_5c = "".join([chr(x ^ 0x5C) for x in xrange(256)])
+trans_36 = "".join([chr(x ^ 0x36) for x in xrange(256)])
+
+
def salted_hmac(key_salt, value, secret=None):
"""
Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a
@@ -27,6 +35,23 @@ def salted_hmac(key_salt, value, secret=None):
# However, we need to ensure that we *always* do this.
return hmac.new(key, msg=value, digestmod=hashlib.sha1)
+
+def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
+ """
+ Returns a random string of length characters from the set of a-z, A-Z, 0-9
+ for use as a salt.
+
+ The default length of 12 with the a-z, A-Z, 0-9 character set returns
+ a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits
+ """
+ import random
+ try:
+ random = random.SystemRandom()
+ except NotImplementedError:
+ pass
+ return ''.join([random.choice(allowed_chars) for i in range(length)])
+
+
def constant_time_compare(val1, val2):
"""
Returns True if the two strings are equal, False otherwise.
@@ -39,3 +64,72 @@ def constant_time_compare(val1, val2):
for x, y in zip(val1, val2):
result |= ord(x) ^ ord(y)
return result == 0
+
+
+def bin_to_long(x):
+ """
+ Convert a binary string into a long integer
+
+ This is a clever optimization for fast xor vector math
+ """
+ return long(x.encode('hex'), 16)
+
+
+def long_to_bin(x):
+ """
+ Convert a long integer into a binary string
+ """
+ hex = "%x" % (x)
+ if len(hex) % 2 == 1:
+ hex = '0' + hex
+ return binascii.unhexlify(hex)
+
+
+def fast_hmac(key, msg, digest):
+ """
+ A trimmed down version of Python's HMAC implementation
+ """
+ dig1, dig2 = digest(), digest()
+ if len(key) > dig1.block_size:
+ key = digest(key).digest()
+ key += chr(0) * (dig1.block_size - len(key))
+ dig1.update(key.translate(trans_36))
+ dig1.update(msg)
+ dig2.update(key.translate(trans_5c))
+ dig2.update(dig1.digest())
+ return dig2
+
+
+def pbkdf2(password, salt, iterations, dklen=0, digest=None):
+ """
+ Implements PBKDF2 as defined in RFC 2898, section 5.2
+
+ HMAC+SHA256 is used as the default pseudo random function.
+
+ Right now 10,000 iterations is the recommended default which takes
+ 100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum
+ for security given 1000 iterations was recommended in 2001. This
+ code is very well optimized for CPython and is only four times
+ slower than openssl's implementation.
+ """
+ assert iterations > 0
+ if not digest:
+ digest = hashlib.sha256
+ hlen = digest().digest_size
+ if not dklen:
+ dklen = hlen
+ if dklen > (2 ** 32 - 1) * hlen:
+ raise OverflowError('dklen too big')
+ l = -(-dklen // hlen)
+ r = dklen - (l - 1) * hlen
+
+ def F(i):
+ def U():
+ u = salt + struct.pack('>I', i)
+ for j in xrange(int(iterations)):
+ u = fast_hmac(password, u, digest).digest()
+ yield bin_to_long(u)
+ return long_to_bin(reduce(operator.xor, U()))
+
+ T = [F(x) for x in range(1, l + 1)]
+ return ''.join(T[:-1]) + T[-1][:r]
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index 029eb3c354..255bdc6850 100644
--- a/docs/releases/1.4.txt
+++ b/docs/releases/1.4.txt
@@ -90,6 +90,22 @@ allows you to fix a very common performance problem in which your code ends up
doing O(n) database queries (or worse) if objects on your primary ``QuerySet``
each have many related objects that you also need.
+Improved password hashing
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django's auth system (``django.contrib.auth``) stores passwords using a one-way
+algorithm. Django 1.3 uses the SHA1_ algorithm, but increasing processor speeds
+and theoretical attacks have revealed that SHA1 isn't as secure as we'd like.
+Thus, Django 1.4 introduces a new password storage system: by default Django now
+uses the PBKDF2_ algorithm (as recommended by NIST_). You can also easily choose
+a different algorithm (including the popular bcrypt_ algorithm). For more
+details, see :ref:`auth_password_storage`.
+
+.. _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
+
HTML5 Doctype
~~~~~~~~~~~~~
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
index 63fe70ff83..1cfccfc5e0 100644
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -371,35 +371,162 @@ Don't set the :attr:`~django.contrib.auth.models.User.password` attribute
directly unless you know what you're doing. This is explained in the next
section.
-Passwords
----------
+.. _auth_password_storage:
+
+How Django stores passwords
+---------------------------
+
+.. versionadded:: 1.4
+ Django 1.4 introduces a new flexible password storage system and uses
+ PBKDF2 by default. Previous versions of Django used SHA1, and other
+ algorithms couldn't be chosen.
The :attr:`~django.contrib.auth.models.User.password` attribute of a
:class:`~django.contrib.auth.models.User` object is a string in this format::
- hashtype$salt$hash
+ algorithm$hash
-That's hashtype, salt and hash, separated by the dollar-sign character.
+That's a storage algorithm, and hash, separated by the dollar-sign
+character. The algorithm is one of a number of one way hashing or password
+storage algorithms Django can use; see below. The hash is the result of the one-
+way function.
-Hashtype is either ``sha1`` (default), ``md5`` or ``crypt`` -- the algorithm
-used to perform a one-way hash of the password. Salt is a random string used
-to salt the raw password to create the hash. Note that the ``crypt`` method is
-only supported on platforms that have the standard Python ``crypt`` module
-available.
+By default, Django uses the PBKDF2_ algorithm with a SHA256 hash, a
+password stretching mechanism recommended by NIST_. This should be
+sufficient for most users: it's quite secure, requiring massive
+amounts of computing time to break.
-For example::
+However, depending on your requirements, you may choose a different
+algorithm, or even use a custom algorithm to match your specific
+security situation. Again, most users shouldn't need to do this -- if
+you're not sure, you probably don't. If you do, please read on:
- sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4
+Django chooses the an algorithm by consulting the :setting:`PASSWORD_HASHERS`
+setting. This is a list of hashing algorithm classes that this Django
+installation supports. The first entry in this list (that is,
+``settings.PASSWORD_HASHERS[0]``) will be used to store passwords, and all the
+other entries are valid hashers that can be used to check existing passwords.
+This means that if you want to use a different algorithm, you'll need to modify
+:setting:`PASSWORD_HASHERS` to list your prefered algorithm first in the list.
-The :meth:`~django.contrib.auth.models.User.set_password` and
-:meth:`~django.contrib.auth.models.User.check_password` functions handle the
-setting and checking of these values behind the scenes.
+The default for :setting:`PASSWORD_HASHERS` is::
-Previous Django versions, such as 0.90, used simple MD5 hashes without password
-salts. For backwards compatibility, those are still supported; they'll be
-converted automatically to the new style the first time
-:meth:`~django.contrib.auth.models.User.check_password()` works correctly for
-a given user.
+ PASSWORD_HASHERS = (
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+ )
+
+This means that Django will use PBKDF2_ to store all passwords, but will support
+checking passwords stored with PBKDF2SHA1, bcrypt_, SHA1_, etc. The next few
+sections describe a couple of common ways advanced users may want to modify this
+setting.
+
+Using bcrypt with Django
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Bcrypt_ is a popular password storage algorithm that's specifically designed
+for long-term password storage. It's not the default used by Django since it
+requires the use of third-party libraries, but since many people may want to
+use it Django supports bcrypt with minimal effort.
+
+To use Bcrypt as your default storage algorithm, do the following:
+
+ 1. Install the `py-bcrypt`_ library (probably by running ``pip install py-bcrypt``,
+ ``easy_install py-bcrypt``, or downloading the library and installing
+ it with ``python setup.py install``).
+
+ 2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptPasswordHasher``
+ first. That is, in your settings file, you'd put::
+
+ PASSWORD_HASHERS = (
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+ )
+
+ (You need to keep the other entries in this list, or else Django won't
+ be able to upgrade passwords; see below).
+
+That's it -- now your Django install will use Bcrypt as the default storage
+algorithm.
+
+.. admonition:: Other bcrypt implementations
+
+ There are several other implementations that allow bcrypt to be
+ used with Django. Django's bcrypt support is NOT directly
+ compatible with these. To upgrade, you will need to modify the
+ hashes in your database to be in the form `bcrypt$(raw bcrypt
+ output)`. For example:
+ `bcrypt$$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy`.
+
+Increasing the work factor
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The PDKDF2 and bcrypt algorithms use a number of iterations or rounds of
+hashing. This deliberately slows down attackers, making attacks against hashed
+passwords harder. However, as computing power increases, the number of
+iterations needs to be increased. We've chosen a reasonable default (and will
+increase it with each release of Django), but you may wish to tune it up or
+down, depending on your security needs and available processing power. To do so,
+you'll subclass the appropriate algorithm and override the ``iterations``
+parameters. For example, to increase the number of iterations used by the
+default PDKDF2 algorithm:
+
+ 1. Create a subclass of ``django.contrib.auth.hashers.PBKDF2PasswordHasher``::
+
+ from django.contrib.auth.hashers import PBKDF2PasswordHasher
+
+ class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher):
+ """
+ A subclass of PBKDF2PasswordHasher that uses 100 times more iterations.
+ """
+ iterations = PBKDF2PasswordHasher.iterations * 100
+
+ Save this somewhere in your project. For example, you might put this in
+ a file like ``myproject/hashers.py``.
+
+ 2. Add your new hasher as the first entry in :setting:`PASSWORD_HASHERS`::
+
+ PASSWORD_HASHERS = (
+ 'myproject.hashers.MyPBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+ )
+
+
+That's it -- now your Django install will use more iterations when it
+stores passwords using PBKDF2.
+
+Password upgrading
+~~~~~~~~~~~~~~~~~~
+
+When users log in, if their passwords are stored with anything other than
+the preferred algorithm, Django will automatically upgrade the algorithm
+to the preferred one. This means that old installs of Django will get
+automatically more secure as users log in, and it also means that you
+can switch to new (and better) storage algorithms as they get invented.
+
+However, Django can only upgrade passwords that use algorithms mentioned in
+:setting:`PASSWORD_HASHERS`, so as you upgrade to new systems you should make
+sure never to *remove* entries from this list. If you do, users using un-
+mentioned algorithms won't be able to upgrade.
+
+.. _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/
Anonymous users
---------------
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
index b80b110585..f48a4adfee 100644
--- a/tests/regressiontests/utils/tests.py
+++ b/tests/regressiontests/utils/tests.py
@@ -1,7 +1,6 @@
"""
Tests for django.utils.
"""
-
from __future__ import absolute_import
from .dateformat import DateFormatTests
@@ -24,4 +23,5 @@ from .baseconv import TestBaseConv
from .jslex import JsTokensTest, JsToCForGettextTest
from .ipv6 import TestUtilsIPv6
from .timezone import TimezoneTests
+from .crypto import TestUtilsCryptoPBKDF2
from .archive import TestZip, TestTar, TestGzipTar, TestBzip2Tar