diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 026e39b7c9..6512e4e3cb 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -516,6 +516,7 @@ PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher', '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 1a93e8945b..a9dbcc9568 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -35,9 +35,14 @@ def check_password(password, encoded, setter=None, preferred='default'): password = smart_str(password) 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 (len(encoded) == 37 and encoded.startswith('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: algorithm = encoded.split('$', 1)[0] 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): """ - I am an incredibly insecure algorithm you should *never* use; - stores unsalted MD5 hashes without the algorithm prefix. + Incredibly insecure algorithm that you should *never* use; stores unsalted + 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 way. Some older Django installs still have these values - lingering around so we need to handle and upgrade them properly. + This class is implemented because Django used to store passwords this way + and 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_md5" @@ -345,6 +384,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher): return '' def encode(self, password, salt): + assert salt == '' return hashlib.md5(password).hexdigest() def verify(self, password, encoded): diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py index 6203e9a899..bf68c45a1b 100644 --- a/django/contrib/auth/tests/hashers.py +++ b/django/contrib/auth/tests/hashers.py @@ -1,5 +1,5 @@ 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, PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD) from django.utils import unittest @@ -31,7 +31,7 @@ class TestUtilsHashPass(unittest.TestCase): def test_pkbdf2(self): encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256') - self.assertEqual(encoded, + self.assertEqual(encoded, 'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') self.assertTrue(is_password_usable(encoded)) self.assertTrue(check_password(u'letmein', encoded)) @@ -39,7 +39,7 @@ class TestUtilsHashPass(unittest.TestCase): def test_sha1(self): encoded = make_password('letmein', 'seasalt', 'sha1') - self.assertEqual(encoded, + self.assertEqual(encoded, 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7') self.assertTrue(is_password_usable(encoded)) self.assertTrue(check_password(u'letmein', encoded)) @@ -47,14 +47,14 @@ class TestUtilsHashPass(unittest.TestCase): def test_md5(self): encoded = make_password('letmein', 'seasalt', 'md5') - self.assertEqual(encoded, + 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') + encoded = make_password('letmein', '', 'unsalted_md5') self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') self.assertTrue(is_password_usable(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.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.") def test_crypt(self): encoded = make_password('letmein', 'ab', 'crypt') @@ -98,14 +108,14 @@ class TestUtilsHashPass(unittest.TestCase): def test_low_level_pkbdf2(self): hasher = PBKDF2PasswordHasher() encoded = hasher.encode('letmein', 'seasalt') - self.assertEqual(encoded, + self.assertEqual(encoded, 'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') self.assertTrue(hasher.verify('letmein', encoded)) def test_low_level_pbkdf2_sha1(self): hasher = PBKDF2SHA1PasswordHasher() encoded = hasher.encode('letmein', 'seasalt') - self.assertEqual(encoded, + self.assertEqual(encoded, 'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=') self.assertTrue(hasher.verify('letmein', encoded))