From 413e37481d0b81d50b5826f660eeb79f360be9fc Mon Sep 17 00:00:00 2001 From: Paul McMillan Date: Wed, 29 Feb 2012 20:12:16 +0000 Subject: [PATCH] Fixes #17777 and makes tests run again. Adds a salted MD5 hasher for backwards compatibility. Thanks gunnar@g10f.de for the report. Also fixes a bug preventing the hasher tests from being run during contrib tests. git-svn-id: http://code.djangoproject.com/svn/django/trunk@17604 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 1 + django/contrib/auth/hashers.py | 38 +++++++++++++++++++++++++--- django/contrib/auth/tests/hashers.py | 13 +++++++--- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 806892d3ca..7bd7d50e70 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -507,6 +507,7 @@ PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', 'django.contrib.auth.hashers.CryptPasswordHasher', ) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index d133bcbfec..58246852a8 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -36,7 +36,7 @@ def check_password(password, encoded, setter=None, preferred='default'): encoded = smart_str(encoded) if len(encoded) == 32 and '$' not in encoded: - hasher = get_hasher('md5') + hasher = get_hasher('unsalted_md5') else: algorithm = encoded.split('$', 1)[0] hasher = get_hasher(algorithm) @@ -69,11 +69,13 @@ def make_password(password, salt=None, hasher='default'): return hasher.encode(password, salt) -def load_hashers(): +def load_hashers(password_hashers=None): global HASHERS global PREFERRED_HASHER hashers = [] - for backend in settings.PASSWORD_HASHERS: + if not password_hashers: + password_hashers = settings.PASSWORD_HASHERS + for backend in password_hashers: try: mod_path, cls_name = backend.rsplit('.', 1) mod = importlib.import_module(mod_path) @@ -300,6 +302,34 @@ class SHA1PasswordHasher(BasePasswordHasher): class MD5PasswordHasher(BasePasswordHasher): + """ + The Salted MD5 password hashing algorithm (not recommended) + """ + algorithm = "md5" + + def encode(self, password, salt): + assert password + assert salt and '$' not in salt + hash = hashlib.md5(salt + password).hexdigest() + return "%s$%s$%s" % (self.algorithm, salt, hash) + + def verify(self, password, encoded): + algorithm, salt, hash = encoded.split('$', 2) + assert algorithm == self.algorithm + encoded_2 = self.encode(password, salt) + return constant_time_compare(encoded, encoded_2) + + def safe_summary(self, encoded): + algorithm, salt, hash = encoded.split('$', 2) + assert algorithm == self.algorithm + return SortedDict([ + (_('algorithm'), algorithm), + (_('salt'), mask_hash(salt, show=2)), + (_('hash'), mask_hash(hash)), + ]) + + +class UnsaltedMD5PasswordHasher(BasePasswordHasher): """ I am an incredibly insecure algorithm you should *never* use; stores unsalted MD5 hashes without the algorithm prefix. @@ -308,7 +338,7 @@ class MD5PasswordHasher(BasePasswordHasher): this way. Some older Django installs still have these values lingering around so we need to handle and upgrade them properly. """ - algorithm = "md5" + algorithm = "unsalted_md5" def salt(self): return '' diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py index 4c66cafe34..865085a194 100644 --- a/django/contrib/auth/tests/hashers.py +++ b/django/contrib/auth/tests/hashers.py @@ -20,7 +20,7 @@ except ImportError: class TestUtilsHashPass(unittest.TestCase): def setUp(self): - load_hashers() + load_hashers(password_hashers=default_hashers) def test_simple(self): encoded = make_password('letmein') @@ -47,6 +47,14 @@ class TestUtilsHashPass(unittest.TestCase): def test_md5(self): encoded = make_password('letmein', 'seasalt', 'md5') + self.assertEqual(encoded, + 'md5$seasalt$f5531bef9f3687d0ccf0f617f0e25573') + self.assertTrue(is_password_usable(encoded)) + self.assertTrue(check_password(u'letmein', encoded)) + self.assertFalse(check_password('letmeinz', encoded)) + + def test_unsalted_md5(self): + encoded = make_password('letmein', 'seasalt', 'unsalted_md5') self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') self.assertTrue(is_password_usable(encoded)) self.assertTrue(check_password(u'letmein', encoded)) @@ -123,6 +131,3 @@ class TestUtilsHashPass(unittest.TestCase): state['upgraded'] = True self.assertFalse(check_password('WRONG', encoded, setter)) self.assertFalse(state['upgraded']) - - -TestUtilsHashPass = override_settings(PASSWORD_HASHERS=default_hashers)(TestUtilsHashPass)