[1.4.x] Fixed #18144 -- Restored compatibility with SHA1 hashes with empty salt.

Thanks dahool for the report and initial version of the patch.

Backport of 633d8de from master.
This commit is contained in:
Aymeric Augustin 2013-02-25 20:01:57 +01:00
parent 52bac4ede1
commit 97a67b26f3
3 changed files with 63 additions and 12 deletions

View File

@ -516,6 +516,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.UnsaltedSHA1PasswordHasher',
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher', 'django.contrib.auth.hashers.CryptPasswordHasher',
) )

View File

@ -35,9 +35,14 @@ def check_password(password, encoded, setter=None, preferred='default'):
password = smart_str(password) password = smart_str(password)
encoded = smart_str(encoded) encoded = smart_str(encoded)
# Ancient versions of Django created plain MD5 passwords and accepted
# MD5 passwords with an empty salt.
if ((len(encoded) == 32 and '$' not in encoded) or if ((len(encoded) == 32 and '$' not in encoded) or
(len(encoded) == 37 and encoded.startswith('md5$$'))): (len(encoded) == 37 and encoded.startswith('md5$$'))):
hasher = get_hasher('unsalted_md5') hasher = get_hasher('unsalted_md5')
# Ancient versions of Django accepted SHA1 passwords with an empty salt.
elif len(encoded) == 46 and encoded.startswith('sha1$$'):
hasher = get_hasher('unsalted_sha1')
else: else:
algorithm = encoded.split('$', 1)[0] algorithm = encoded.split('$', 1)[0]
hasher = get_hasher(algorithm) hasher = get_hasher(algorithm)
@ -330,14 +335,48 @@ class MD5PasswordHasher(BasePasswordHasher):
]) ])
class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
"""
Very insecure algorithm that you should *never* use; stores SHA1 hashes
with an empty salt.
This class is implemented because Django used to accept such password
hashes. Some older Django installs still have these values lingering
around so we need to handle and upgrade them properly.
"""
algorithm = "unsalted_sha1"
def salt(self):
return ''
def encode(self, password, salt):
assert salt == ''
hash = hashlib.sha1(password).hexdigest()
return 'sha1$$%s' % hash
def verify(self, password, encoded):
encoded_2 = self.encode(password, '')
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
assert encoded.startswith('sha1$$')
hash = encoded[6:]
return SortedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(hash)),
])
class UnsaltedMD5PasswordHasher(BasePasswordHasher): class UnsaltedMD5PasswordHasher(BasePasswordHasher):
""" """
I am an incredibly insecure algorithm you should *never* use; Incredibly insecure algorithm that you should *never* use; stores unsalted
stores unsalted MD5 hashes without the algorithm prefix. MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
empty salt.
This class is implemented because Django used to store passwords This class is implemented because Django used to store passwords this way
this way. Some older Django installs still have these values and to accept such password hashes. Some older Django installs still have
lingering around so we need to handle and upgrade them properly. these values lingering around so we need to handle and upgrade them
properly.
""" """
algorithm = "unsalted_md5" algorithm = "unsalted_md5"
@ -345,6 +384,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
return '' return ''
def encode(self, password, salt): def encode(self, password, salt):
assert salt == ''
return hashlib.md5(password).hexdigest() return hashlib.md5(password).hexdigest()
def verify(self, password, encoded): def verify(self, password, encoded):

View File

@ -54,7 +54,7 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertFalse(check_password('letmeinz', encoded)) self.assertFalse(check_password('letmeinz', encoded))
def test_unsalted_md5(self): def test_unsalted_md5(self):
encoded = make_password('letmein', 'seasalt', 'unsalted_md5') encoded = make_password('letmein', '', '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))
@ -65,6 +65,16 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password(u'letmein', alt_encoded)) self.assertTrue(check_password(u'letmein', alt_encoded))
self.assertFalse(check_password('letmeinz', alt_encoded)) self.assertFalse(check_password('letmeinz', alt_encoded))
def test_unsalted_sha1(self):
encoded = make_password('letmein', '', 'unsalted_sha1')
self.assertEqual(encoded, 'sha1$$b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('letmein', encoded))
self.assertFalse(check_password('letmeinz', encoded))
# Raw SHA1 isn't acceptable
alt_encoded = encoded[6:]
self.assertRaises(ValueError, check_password, 'letmein', alt_encoded)
@skipUnless(crypt, "no crypt module to generate password.") @skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self): def test_crypt(self):
encoded = make_password('letmein', 'ab', 'crypt') encoded = make_password('letmein', 'ab', 'crypt')