Skip to content

Commit

Permalink
Renovated password hashing. Many thanks to Justine Tunney for help wi…
Browse files Browse the repository at this point in the history
…th the initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17253 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
PaulMcMillan committed Dec 23, 2011
1 parent a976159 commit dce820f
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 89 deletions.
12 changes: 12 additions & 0 deletions django/conf/global_settings.py
Expand Up @@ -498,6 +498,18 @@
# 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 #
########### ###########
Expand Down
40 changes: 20 additions & 20 deletions django/contrib/auth/forms.py
@@ -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


Expand All @@ -18,27 +19,26 @@


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):
Expand Down
25 changes: 9 additions & 16 deletions django/contrib/auth/models.py
Expand Up @@ -9,11 +9,10 @@
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):
Expand Down Expand Up @@ -220,27 +219,21 @@ def get_full_name(self):
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. self.set_password(raw_password)
if '$' not in self.password: self.save()
is_correct = (self.password == get_hexdigest('md5', '', raw_password)) return check_password(raw_password, self.password, setter)
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 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)
Expand Down
8 changes: 5 additions & 3 deletions django/contrib/auth/tests/__init__.py
@@ -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,
Expand All @@ -11,9 +11,11 @@
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'
28 changes: 0 additions & 28 deletions django/contrib/auth/tests/basic.py
@@ -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


Expand Down Expand Up @@ -111,30 +110,3 @@ def test_createsuperuser_management_command(self):
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")
96 changes: 95 additions & 1 deletion django/utils/crypto.py
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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]
16 changes: 16 additions & 0 deletions docs/releases/1.4.txt
Expand Up @@ -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
~~~~~~~~~~~~~ ~~~~~~~~~~~~~


Expand Down

0 comments on commit dce820f

Please sign in to comment.