Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Renovated password hashing. Many thanks to Justine Tunney for help wi…

…th the initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17253 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit dce820ff70f00e974afd3e6e310aa825bc55319f 1 parent a976159
@PaulMcMillan PaulMcMillan authored
View
12 django/conf/global_settings.py
@@ -498,6 +498,18 @@
# 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 #
###########
View
40 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 @@
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("""<div%(attrs)s>
- <strong>%(hash_type_label)s</strong>: %(hash_type)s
- <strong>%(masked_label)s</strong>: %(masked)s
- </div>""" % {
- "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 += "<strong>%(key)s</strong>: %(value)s " % {"key": key, "value": value}
+
+ return mark_safe("<div%(attrs)s>%(summary)s</div>" % {"attrs": flatatt(final_attrs), "summary": summary})
class ReadOnlyPasswordHashField(forms.Field):
View
25 django/contrib/auth/models.py
@@ -9,11 +9,10 @@
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 @@ def get_full_name(self):
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)
View
8 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 @@
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'
View
28 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 @@ def test_createsuperuser_management_command(self):
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")
View
96 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]
View
16 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
~~~~~~~~~~~~~
View
167 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
-
-That's hashtype, salt and hash, separated by the dollar-sign character.
+ algorithm$hash
+
+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.
+
+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.
+
+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:
+
+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 default for :setting:`PASSWORD_HASHERS` is::
+
+ 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',
+ )
-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.
+ (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',
+ )
-For example::
- sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4
+That's it -- now your Django install will use more iterations when it
+stores passwords using PBKDF2.
-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.
+Password upgrading
+~~~~~~~~~~~~~~~~~~
-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.
+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
---------------
View
2  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 .jslex import JsTokensTest, JsToCForGettextTest
from .ipv6 import TestUtilsIPv6
from .timezone import TimezoneTests
+from .crypto import TestUtilsCryptoPBKDF2
from .archive import TestZip, TestTar, TestGzipTar, TestBzip2Tar
Please sign in to comment.
Something went wrong with that request. Please try again.