Commit dce820ff authored by Paul McMillan's avatar Paul McMillan
Browse files

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
parent a976159d
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -498,6 +498,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
# 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 #
###########
+20 −20
Original line number Diff line number Diff line
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 @@ mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p

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 −16
Original line number Diff line number Diff line
@@ -9,11 +9,10 @@ from django.utils.translation import ugettext_lazy as _
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 @@ class User(models.Model):
        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.
        def setter(raw_password):
            self.set_password(raw_password)
            self.save()
            return is_correct
        return check_password(raw_password, self.password)
        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)
+5 −3
Original line number Diff line number Diff line
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 @@ from django.contrib.auth.tests.remote_user import (RemoteUserTest,
    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'
+0 −28
Original line number Diff line number Diff line
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 @@ class BasicTestCase(TestCase):
        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")
Loading