[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:
parent
52bac4ede1
commit
97a67b26f3
@ -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',
|
||||||
)
|
)
|
||||||
|
@ -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):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
|
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
|
||||||
from django.contrib.auth.hashers import (is_password_usable,
|
from django.contrib.auth.hashers import (is_password_usable,
|
||||||
check_password, make_password, PBKDF2PasswordHasher, load_hashers,
|
check_password, make_password, PBKDF2PasswordHasher, load_hashers,
|
||||||
PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD)
|
PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD)
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
@ -31,7 +31,7 @@ class TestUtilsHashPass(unittest.TestCase):
|
|||||||
|
|
||||||
def test_pkbdf2(self):
|
def test_pkbdf2(self):
|
||||||
encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256')
|
encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256')
|
||||||
self.assertEqual(encoded,
|
self.assertEqual(encoded,
|
||||||
'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
|
'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
|
||||||
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))
|
||||||
@ -39,7 +39,7 @@ class TestUtilsHashPass(unittest.TestCase):
|
|||||||
|
|
||||||
def test_sha1(self):
|
def test_sha1(self):
|
||||||
encoded = make_password('letmein', 'seasalt', 'sha1')
|
encoded = make_password('letmein', 'seasalt', 'sha1')
|
||||||
self.assertEqual(encoded,
|
self.assertEqual(encoded,
|
||||||
'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7')
|
'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7')
|
||||||
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))
|
||||||
@ -47,14 +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,
|
self.assertEqual(encoded,
|
||||||
'md5$seasalt$f5531bef9f3687d0ccf0f617f0e25573')
|
'md5$seasalt$f5531bef9f3687d0ccf0f617f0e25573')
|
||||||
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))
|
||||||
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')
|
||||||
@ -98,14 +108,14 @@ class TestUtilsHashPass(unittest.TestCase):
|
|||||||
def test_low_level_pkbdf2(self):
|
def test_low_level_pkbdf2(self):
|
||||||
hasher = PBKDF2PasswordHasher()
|
hasher = PBKDF2PasswordHasher()
|
||||||
encoded = hasher.encode('letmein', 'seasalt')
|
encoded = hasher.encode('letmein', 'seasalt')
|
||||||
self.assertEqual(encoded,
|
self.assertEqual(encoded,
|
||||||
'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
|
'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
|
||||||
self.assertTrue(hasher.verify('letmein', encoded))
|
self.assertTrue(hasher.verify('letmein', encoded))
|
||||||
|
|
||||||
def test_low_level_pbkdf2_sha1(self):
|
def test_low_level_pbkdf2_sha1(self):
|
||||||
hasher = PBKDF2SHA1PasswordHasher()
|
hasher = PBKDF2SHA1PasswordHasher()
|
||||||
encoded = hasher.encode('letmein', 'seasalt')
|
encoded = hasher.encode('letmein', 'seasalt')
|
||||||
self.assertEqual(encoded,
|
self.assertEqual(encoded,
|
||||||
'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=')
|
'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=')
|
||||||
self.assertTrue(hasher.verify('letmein', encoded))
|
self.assertTrue(hasher.verify('letmein', encoded))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user