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:
parent
a976159db0
commit
dce820ff70
|
@ -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 #
|
||||||
###########
|
###########
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
---------------
|
---------------
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue