Commit 70a0351f authored by Claude Paroz's avatar Claude Paroz
Browse files

Fixed #18184 -- Moved algorithm identification code to hashers module

Thanks Eli Collins for the report and the patch.
parent eb286aa2
Loading
Loading
Loading
Loading
+2 −7
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _

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.hashers import UNUSABLE_PASSWORD, is_password_usable, identify_hasher
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site

@@ -25,13 +25,8 @@ class ReadOnlyPasswordHashWidget(forms.Widget):

        final_attrs = self.build_attrs(attrs)

        if len(encoded) == 32 and '$' not in encoded:
            algorithm = 'unsalted_md5'
        else:
            algorithm = encoded.split('$', 1)[0]

        try:
            hasher = get_hasher(algorithm)
            hasher = identify_hasher(encoded)
        except ValueError:
            summary = "<strong>Invalid password format or unknown hashing algorithm.</strong>"
        else:
+16 −6
Original line number Diff line number Diff line
@@ -40,12 +40,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
        return False

    preferred = get_hasher(preferred)

    if len(encoded) == 32 and '$' not in encoded:
        hasher = get_hasher('unsalted_md5')
    else:
        algorithm = encoded.split('$', 1)[0]
        hasher = get_hasher(algorithm)
    hasher = identify_hasher(encoded)

    must_update = hasher.algorithm != preferred.algorithm
    is_correct = hasher.verify(password, encoded)
@@ -120,6 +115,21 @@ def get_hasher(algorithm='default'):
        return HASHERS[algorithm]


def identify_hasher(encoded):
    """
    Returns an instance of a loaded password hasher.

    Identifies hasher algorithm by examining encoded hash, and calls
    get_hasher() to return hasher. Raises ValueError if
    algorithm cannot be identified, or if hasher is not loaded.
    """
    if len(encoded) == 32 and '$' not in encoded:
        algorithm = 'unsalted_md5'
    else:
        algorithm = encoded.split('$', 1)[0]
    return get_hasher(algorithm)


def mask_hash(hash, show=6, char="*"):
    """
    Returns the given hash, with only the first ``show`` number shown. The
+9 −1
Original line number Diff line number Diff line
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
from django.contrib.auth.hashers import (is_password_usable, 
    check_password, make_password, PBKDF2PasswordHasher, load_hashers,
    PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD)
    PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
from django.utils import unittest
from django.utils.unittest import skipUnless

@@ -36,6 +36,7 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertTrue(is_password_usable(encoded))
        self.assertTrue(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")

    def test_sha1(self):
        encoded = make_password('letmein', 'seasalt', 'sha1')
@@ -44,6 +45,7 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertTrue(is_password_usable(encoded))
        self.assertTrue(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertEqual(identify_hasher(encoded).algorithm, "sha1")

    def test_md5(self):
        encoded = make_password('letmein', 'seasalt', 'md5')
@@ -52,6 +54,7 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertTrue(is_password_usable(encoded))
        self.assertTrue(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertEqual(identify_hasher(encoded).algorithm, "md5")

    def test_unsalted_md5(self):
        encoded = make_password('letmein', 'seasalt', 'unsalted_md5')
@@ -59,6 +62,7 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertTrue(is_password_usable(encoded))
        self.assertTrue(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5")

    @skipUnless(crypt, "no crypt module to generate password.")
    def test_crypt(self):
@@ -67,6 +71,7 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertTrue(is_password_usable(encoded))
        self.assertTrue(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertEqual(identify_hasher(encoded).algorithm, "crypt")

    @skipUnless(bcrypt, "py-bcrypt not installed")
    def test_bcrypt(self):
@@ -75,6 +80,7 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertTrue(encoded.startswith('bcrypt$'))
        self.assertTrue(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")

    def test_unusable(self):
        encoded = make_password(None)
@@ -84,11 +90,13 @@ class TestUtilsHashPass(unittest.TestCase):
        self.assertFalse(check_password('', encoded))
        self.assertFalse(check_password(u'letmein', encoded))
        self.assertFalse(check_password('letmeinz', encoded))
        self.assertRaises(ValueError, identify_hasher, encoded)

    def test_bad_algorithm(self):
        def doit():
            make_password('letmein', hasher='lolcat')
        self.assertRaises(ValueError, doit)
        self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")

    def test_low_level_pkbdf2(self):
        hasher = PBKDF2PasswordHasher()