Permalink
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...
1 parent a976159 commit dce820ff70f00e974afd3e6e310aa825bc55319f @PaulMcMillan PaulMcMillan committed Dec 23, 2011
@@ -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 #
###########
@@ -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):
@@ -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)
@@ -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'
@@ -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")
@@ -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
@@ -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
~~~~~~~~~~~~~
Oops, something went wrong.

0 comments on commit dce820f

Please sign in to comment.