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
This commit is contained in:
Paul McMillan 2012-02-29 20:12:16 +00:00
parent ae640e5ea0
commit 413e37481d
3 changed files with 44 additions and 8 deletions

View File

@ -507,6 +507,7 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher', 'django.contrib.auth.hashers.CryptPasswordHasher',
) )

View File

@ -36,7 +36,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
encoded = smart_str(encoded) encoded = smart_str(encoded)
if len(encoded) == 32 and '$' not in encoded: if len(encoded) == 32 and '$' not in encoded:
hasher = get_hasher('md5') hasher = get_hasher('unsalted_md5')
else: else:
algorithm = encoded.split('$', 1)[0] algorithm = encoded.split('$', 1)[0]
hasher = get_hasher(algorithm) hasher = get_hasher(algorithm)
@ -69,11 +69,13 @@ def make_password(password, salt=None, hasher='default'):
return hasher.encode(password, salt) return hasher.encode(password, salt)
def load_hashers(): def load_hashers(password_hashers=None):
global HASHERS global HASHERS
global PREFERRED_HASHER global PREFERRED_HASHER
hashers = [] hashers = []
for backend in settings.PASSWORD_HASHERS: if not password_hashers:
password_hashers = settings.PASSWORD_HASHERS
for backend in password_hashers:
try: try:
mod_path, cls_name = backend.rsplit('.', 1) mod_path, cls_name = backend.rsplit('.', 1)
mod = importlib.import_module(mod_path) mod = importlib.import_module(mod_path)
@ -300,6 +302,34 @@ class SHA1PasswordHasher(BasePasswordHasher):
class MD5PasswordHasher(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; I am an incredibly insecure algorithm you should *never* use;
stores unsalted MD5 hashes without the algorithm prefix. 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 this way. Some older Django installs still have these values
lingering around so we need to handle and upgrade them properly. lingering around so we need to handle and upgrade them properly.
""" """
algorithm = "md5" algorithm = "unsalted_md5"
def salt(self): def salt(self):
return '' return ''

View File

@ -20,7 +20,7 @@ except ImportError:
class TestUtilsHashPass(unittest.TestCase): class TestUtilsHashPass(unittest.TestCase):
def setUp(self): def setUp(self):
load_hashers() load_hashers(password_hashers=default_hashers)
def test_simple(self): def test_simple(self):
encoded = make_password('letmein') encoded = make_password('letmein')
@ -47,6 +47,14 @@ class TestUtilsHashPass(unittest.TestCase):
def test_md5(self): def test_md5(self):
encoded = make_password('letmein', 'seasalt', 'md5') 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.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7')
self.assertTrue(is_password_usable(encoded)) self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password(u'letmein', encoded)) self.assertTrue(check_password(u'letmein', encoded))
@ -123,6 +131,3 @@ class TestUtilsHashPass(unittest.TestCase):
state['upgraded'] = True state['upgraded'] = True
self.assertFalse(check_password('WRONG', encoded, setter)) self.assertFalse(check_password('WRONG', encoded, setter))
self.assertFalse(state['upgraded']) self.assertFalse(state['upgraded'])
TestUtilsHashPass = override_settings(PASSWORD_HASHERS=default_hashers)(TestUtilsHashPass)