Renovated password hashing. Many thanks to Justine Tunney for help with the initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17253 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Paul McMillan 2011-12-23 03:46:06 +00:00
parent a976159db0
commit dce820ff70
9 changed files with 304 additions and 88 deletions

View File

@ -498,6 +498,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
# The number of days a password reset link is valid for # The number of days a password reset link is valid for
PASSWORD_RESET_TIMEOUT_DAYS = 3 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 # # SIGNING #
########### ###########

View File

@ -1,13 +1,14 @@
from django import forms from django import forms
from django.forms.util import flatatt from django.forms.util import flatatt
from django.template import loader from django.template import loader
from django.utils.encoding import smart_str
from django.utils.http import int_to_base36 from django.utils.http import int_to_base36
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ 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 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.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site 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): class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs): def render(self, name, value, attrs):
if not value: encoded = value
if not is_password_usable(encoded):
return "None" return "None"
final_attrs = self.build_attrs(attrs) final_attrs = self.build_attrs(attrs)
parts = value.split("$")
if len(parts) != 3: encoded = smart_str(encoded)
# Legacy passwords didn't specify a hash type and were md5.
hash_type = "md5" if len(encoded) == 32 and '$' not in encoded:
masked = mask_password(value) hasher = get_hasher('md5')
else: else:
hash_type = parts[0] algorithm = encoded.split('$', 1)[0]
masked = mask_password(parts[2]) hasher = get_hasher(algorithm)
return mark_safe("""<div%(attrs)s>
<strong>%(hash_type_label)s</strong>: %(hash_type)s summary = ""
<strong>%(masked_label)s</strong>: %(masked)s for key, value in hasher.safe_summary(encoded).iteritems():
</div>""" % { summary += "<strong>%(key)s</strong>: %(value)s " % {"key": key, "value": value}
"attrs": flatatt(final_attrs),
"hash_type_label": _("Hash type"), return mark_safe("<div%(attrs)s>%(summary)s</div>" % {"attrs": flatatt(final_attrs), "summary": summary})
"hash_type": hash_type,
"masked_label": _("Masked hash"),
"masked": masked,
})
class ReadOnlyPasswordHashField(forms.Field): class ReadOnlyPasswordHashField(forms.Field):

View File

@ -9,11 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.contrib import auth from django.contrib import auth
from django.contrib.auth.signals import user_logged_in
# UNUSABLE_PASSWORD is still imported here for backwards compatibility # UNUSABLE_PASSWORD is still imported here for backwards compatibility
from django.contrib.auth.utils import (get_hexdigest, make_password, from django.contrib.auth.hashers import (
check_password, is_password_usable, get_random_string, check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
UNUSABLE_PASSWORD) from django.contrib.auth.signals import user_logged_in
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
def update_last_login(sender, user, **kwargs): def update_last_login(sender, user, **kwargs):
@ -220,27 +219,21 @@ class User(models.Model):
return full_name.strip() return full_name.strip()
def set_password(self, raw_password): 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): def check_password(self, raw_password):
""" """
Returns a boolean of whether the raw_password was correct. Handles Returns a boolean of whether the raw_password was correct. Handles
hashing formats behind the scenes. hashing formats behind the scenes.
""" """
# Backwards-compatibility check. Older passwords won't include the def setter(raw_password):
# 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.set_password(raw_password)
self.save() self.save()
return is_correct return check_password(raw_password, self.password, setter)
return check_password(raw_password, self.password)
def set_unusable_password(self): def set_unusable_password(self):
# Sets a value that will never be a valid hash # 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): def has_usable_password(self):
return is_password_usable(self.password) return is_password_usable(self.password)

View File

@ -1,7 +1,7 @@
from django.contrib.auth.tests.auth_backends import (BackendTest, from django.contrib.auth.tests.auth_backends import (BackendTest,
RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest,
InActiveUserBackendTest, NoInActiveUserBackendTest) 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.context_processors import AuthContextProcessorTests
from django.contrib.auth.tests.decorators import LoginRequiredTestCase from django.contrib.auth.tests.decorators import LoginRequiredTestCase
from django.contrib.auth.tests.forms import (UserCreationFormTest, from django.contrib.auth.tests.forms import (UserCreationFormTest,
@ -11,9 +11,11 @@ from django.contrib.auth.tests.remote_user import (RemoteUserTest,
RemoteUserNoCreateTest, RemoteUserCustomTest) RemoteUserNoCreateTest, RemoteUserCustomTest)
from django.contrib.auth.tests.management import GetDefaultUsernameTestCase from django.contrib.auth.tests.management import GetDefaultUsernameTestCase
from django.contrib.auth.tests.models import ProfileTestCase 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.signals import SignalTestCase
from django.contrib.auth.tests.tokens import TokenGeneratorTest from django.contrib.auth.tests.tokens import TokenGeneratorTest
from django.contrib.auth.tests.views import (AuthViewNamedURLTests, PasswordResetTest, from django.contrib.auth.tests.views import (AuthViewNamedURLTests,
ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings) PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest,
LoginURLSettings)
# The password for the fixture data users is 'password' # The password for the fixture data users is 'password'

View File

@ -1,7 +1,6 @@
from django.test import TestCase from django.test import TestCase
from django.utils.unittest import skipUnless from django.utils.unittest import skipUnless
from django.contrib.auth.models import User, AnonymousUser from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth import utils
from django.core.management import call_command from django.core.management import call_command
from StringIO import StringIO from StringIO import StringIO
@ -111,30 +110,3 @@ class BasicTestCase(TestCase):
u = User.objects.get(username="joe+admin@somewhere.org") u = User.objects.get(username="joe+admin@somewhere.org")
self.assertEqual(u.email, 'joe@somewhere.org') self.assertEqual(u.email, 'joe@somewhere.org')
self.assertFalse(u.has_usable_password()) 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 File

@ -2,10 +2,18 @@
Django's standard crypto functions and utilities. Django's standard crypto functions and utilities.
""" """
import hashlib
import hmac import hmac
import struct
import hashlib
import binascii
import operator
from django.conf import settings 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): def salted_hmac(key_salt, value, secret=None):
""" """
Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a 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. # However, we need to ensure that we *always* do this.
return hmac.new(key, msg=value, digestmod=hashlib.sha1) 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): def constant_time_compare(val1, val2):
""" """
Returns True if the two strings are equal, False otherwise. 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): for x, y in zip(val1, val2):
result |= ord(x) ^ ord(y) result |= ord(x) ^ ord(y)
return result == 0 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 File

@ -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`` doing O(n) database queries (or worse) if objects on your primary ``QuerySet``
each have many related objects that you also need. 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 HTML5 Doctype
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -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 directly unless you know what you're doing. This is explained in the next
section. 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 The :attr:`~django.contrib.auth.models.User.password` attribute of a
:class:`~django.contrib.auth.models.User` object is a string in this format:: :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 By default, Django uses the PBKDF2_ algorithm with a SHA256 hash, a
used to perform a one-way hash of the password. Salt is a random string used password stretching mechanism recommended by NIST_. This should be
to salt the raw password to create the hash. Note that the ``crypt`` method is sufficient for most users: it's quite secure, requiring massive
only supported on platforms that have the standard Python ``crypt`` module amounts of computing time to break.
available.
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 The default for :setting:`PASSWORD_HASHERS` is::
:meth:`~django.contrib.auth.models.User.check_password` functions handle the
setting and checking of these values behind the scenes.
Previous Django versions, such as 0.90, used simple MD5 hashes without password PASSWORD_HASHERS = (
salts. For backwards compatibility, those are still supported; they'll be 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
converted automatically to the new style the first time 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
:meth:`~django.contrib.auth.models.User.check_password()` works correctly for 'django.contrib.auth.hashers.BCryptPasswordHasher',
a given user. '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 Anonymous users
--------------- ---------------

View File

@ -1,7 +1,6 @@
""" """
Tests for django.utils. Tests for django.utils.
""" """
from __future__ import absolute_import from __future__ import absolute_import
from .dateformat import DateFormatTests from .dateformat import DateFormatTests
@ -24,4 +23,5 @@ from .baseconv import TestBaseConv
from .jslex import JsTokensTest, JsToCForGettextTest from .jslex import JsTokensTest, JsToCForGettextTest
from .ipv6 import TestUtilsIPv6 from .ipv6 import TestUtilsIPv6
from .timezone import TimezoneTests from .timezone import TimezoneTests
from .crypto import TestUtilsCryptoPBKDF2
from .archive import TestZip, TestTar, TestGzipTar, TestBzip2Tar from .archive import TestZip, TestTar, TestGzipTar, TestBzip2Tar