Fixed #18144 -- Restored compatibility with SHA1 hashes with empty salt.

Thanks dahool for the report and initial version of the patch.
This commit is contained in:
Aymeric Augustin 2013-02-25 20:01:57 +01:00
parent 509798ae06
commit f1255a3c09
3 changed files with 60 additions and 8 deletions

View File

@ -510,6 +510,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

@ -127,9 +127,14 @@ def identify_hasher(encoded):
get_hasher() to return hasher. Raises ValueError if get_hasher() to return hasher. Raises ValueError if
algorithm cannot be identified, or if hasher is not loaded. algorithm cannot be identified, or if hasher is not loaded.
""" """
# 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$$'))):
algorithm = 'unsalted_md5' algorithm = 'unsalted_md5'
# Ancient versions of Django accepted SHA1 passwords with an empty salt.
elif len(encoded) == 46 and encoded.startswith('sha1$$'):
algorithm = 'unsalted_sha1'
else: else:
algorithm = encoded.split('$', 1)[0] algorithm = encoded.split('$', 1)[0]
return get_hasher(algorithm) return get_hasher(algorithm)
@ -350,14 +355,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(force_bytes(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"
@ -365,6 +404,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
return '' return ''
def encode(self, password, salt): def encode(self, password, salt):
assert salt == ''
return hashlib.md5(force_bytes(password)).hexdigest() return hashlib.md5(force_bytes(password)).hexdigest()
def verify(self, password, encoded): def verify(self, password, encoded):

View File

@ -60,7 +60,7 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertEqual(identify_hasher(encoded).algorithm, "md5") self.assertEqual(identify_hasher(encoded).algorithm, "md5")
def test_unsalted_md5(self): def test_unsalted_md5(self):
encoded = make_password('lètmein', 'seasalt', 'unsalted_md5') encoded = make_password('lètmein', '', 'unsalted_md5')
self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43') self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
self.assertTrue(is_password_usable(encoded)) self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded)) self.assertTrue(check_password('lètmein', encoded))
@ -72,6 +72,17 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', alt_encoded)) self.assertTrue(check_password('lètmein', alt_encoded))
self.assertFalse(check_password('lètmeinz', alt_encoded)) self.assertFalse(check_password('lètmeinz', alt_encoded))
def test_unsalted_sha1(self):
encoded = make_password('lètmein', '', 'unsalted_sha1')
self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
# Raw SHA1 isn't acceptable
alt_encoded = encoded[6:]
self.assertFalse(check_password('lètmein', 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('lètmei', 'ab', 'crypt') encoded = make_password('lètmei', 'ab', 'crypt')