mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Refs #33691 -- Removed insecure password hashers per deprecation timeline.
This commit is contained in:
		| @@ -16,7 +16,6 @@ from django.utils.crypto import ( | ||||
|     get_random_string, | ||||
|     pbkdf2, | ||||
| ) | ||||
| from django.utils.deprecation import RemovedInDjango51Warning | ||||
| from django.utils.module_loading import import_string | ||||
| from django.utils.translation import gettext_noop as _ | ||||
|  | ||||
| @@ -641,57 +640,6 @@ class ScryptPasswordHasher(BasePasswordHasher): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| # RemovedInDjango51Warning. | ||||
| class SHA1PasswordHasher(BasePasswordHasher): | ||||
|     """ | ||||
|     The SHA1 password hashing algorithm (not recommended) | ||||
|     """ | ||||
|  | ||||
|     algorithm = "sha1" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         warnings.warn( | ||||
|             "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated.", | ||||
|             RemovedInDjango51Warning, | ||||
|             stacklevel=2, | ||||
|         ) | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     def encode(self, password, salt): | ||||
|         self._check_encode_args(password, salt) | ||||
|         hash = hashlib.sha1((salt + password).encode()).hexdigest() | ||||
|         return "%s$%s$%s" % (self.algorithm, salt, hash) | ||||
|  | ||||
|     def decode(self, encoded): | ||||
|         algorithm, salt, hash = encoded.split("$", 2) | ||||
|         assert algorithm == self.algorithm | ||||
|         return { | ||||
|             "algorithm": algorithm, | ||||
|             "hash": hash, | ||||
|             "salt": salt, | ||||
|         } | ||||
|  | ||||
|     def verify(self, password, encoded): | ||||
|         decoded = self.decode(encoded) | ||||
|         encoded_2 = self.encode(password, decoded["salt"]) | ||||
|         return constant_time_compare(encoded, encoded_2) | ||||
|  | ||||
|     def safe_summary(self, encoded): | ||||
|         decoded = self.decode(encoded) | ||||
|         return { | ||||
|             _("algorithm"): decoded["algorithm"], | ||||
|             _("salt"): mask_hash(decoded["salt"], show=2), | ||||
|             _("hash"): mask_hash(decoded["hash"]), | ||||
|         } | ||||
|  | ||||
|     def must_update(self, encoded): | ||||
|         decoded = self.decode(encoded) | ||||
|         return must_update_salt(decoded["salt"], self.salt_entropy) | ||||
|  | ||||
|     def harden_runtime(self, password, encoded): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| class MD5PasswordHasher(BasePasswordHasher): | ||||
|     """ | ||||
|     The Salted MD5 password hashing algorithm (not recommended) | ||||
| @@ -732,111 +680,3 @@ class MD5PasswordHasher(BasePasswordHasher): | ||||
|  | ||||
|     def harden_runtime(self, password, encoded): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| # RemovedInDjango51Warning. | ||||
| class UnsaltedSHA1PasswordHasher(BasePasswordHasher): | ||||
|     """ | ||||
|     Very insecure algorithm that you should *never* use; store 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 __init__(self, *args, **kwargs): | ||||
|         warnings.warn( | ||||
|             "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated.", | ||||
|             RemovedInDjango51Warning, | ||||
|             stacklevel=2, | ||||
|         ) | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     def salt(self): | ||||
|         return "" | ||||
|  | ||||
|     def encode(self, password, salt): | ||||
|         if salt != "": | ||||
|             raise ValueError("salt must be empty.") | ||||
|         hash = hashlib.sha1(password.encode()).hexdigest() | ||||
|         return "sha1$$%s" % hash | ||||
|  | ||||
|     def decode(self, encoded): | ||||
|         assert encoded.startswith("sha1$$") | ||||
|         return { | ||||
|             "algorithm": self.algorithm, | ||||
|             "hash": encoded[6:], | ||||
|             "salt": None, | ||||
|         } | ||||
|  | ||||
|     def verify(self, password, encoded): | ||||
|         encoded_2 = self.encode(password, "") | ||||
|         return constant_time_compare(encoded, encoded_2) | ||||
|  | ||||
|     def safe_summary(self, encoded): | ||||
|         decoded = self.decode(encoded) | ||||
|         return { | ||||
|             _("algorithm"): decoded["algorithm"], | ||||
|             _("hash"): mask_hash(decoded["hash"]), | ||||
|         } | ||||
|  | ||||
|     def harden_runtime(self, password, encoded): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| # RemovedInDjango51Warning. | ||||
| class UnsaltedMD5PasswordHasher(BasePasswordHasher): | ||||
|     """ | ||||
|     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 | ||||
|     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" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         warnings.warn( | ||||
|             "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated.", | ||||
|             RemovedInDjango51Warning, | ||||
|             stacklevel=2, | ||||
|         ) | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     def salt(self): | ||||
|         return "" | ||||
|  | ||||
|     def encode(self, password, salt): | ||||
|         if salt != "": | ||||
|             raise ValueError("salt must be empty.") | ||||
|         return hashlib.md5(password.encode()).hexdigest() | ||||
|  | ||||
|     def decode(self, encoded): | ||||
|         return { | ||||
|             "algorithm": self.algorithm, | ||||
|             "hash": encoded, | ||||
|             "salt": None, | ||||
|         } | ||||
|  | ||||
|     def verify(self, password, encoded): | ||||
|         if len(encoded) == 37: | ||||
|             encoded = encoded.removeprefix("md5$$") | ||||
|         encoded_2 = self.encode(password, "") | ||||
|         return constant_time_compare(encoded, encoded_2) | ||||
|  | ||||
|     def safe_summary(self, encoded): | ||||
|         decoded = self.decode(encoded) | ||||
|         return { | ||||
|             _("algorithm"): decoded["algorithm"], | ||||
|             _("hash"): mask_hash(decoded["hash"], show=3), | ||||
|         } | ||||
|  | ||||
|     def harden_runtime(self, password, encoded): | ||||
|         pass | ||||
|   | ||||
| @@ -256,3 +256,7 @@ to remove usage of these features. | ||||
| * The model's ``Meta.index_together`` option is removed. | ||||
|  | ||||
| * The ``length_is`` template filter is removed. | ||||
|  | ||||
| * The ``django.contrib.auth.hashers.SHA1PasswordHasher``, | ||||
|   ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and | ||||
|   ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` are removed. | ||||
|   | ||||
| @@ -18,9 +18,8 @@ from django.contrib.auth.hashers import ( | ||||
|     is_password_usable, | ||||
|     make_password, | ||||
| ) | ||||
| from django.test import SimpleTestCase, ignore_warnings | ||||
| from django.test import SimpleTestCase | ||||
| from django.test.utils import override_settings | ||||
| from django.utils.deprecation import RemovedInDjango51Warning | ||||
|  | ||||
| try: | ||||
|     import bcrypt | ||||
| @@ -103,40 +102,6 @@ class TestUtilsHashPass(SimpleTestCase): | ||||
|         self.assertIs(hasher.must_update(encoded_weak_salt), True) | ||||
|         self.assertIs(hasher.must_update(encoded_strong_salt), False) | ||||
|  | ||||
|     @ignore_warnings(category=RemovedInDjango51Warning) | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"] | ||||
|     ) | ||||
|     def test_sha1(self): | ||||
|         encoded = make_password("lètmein", "seasalt", "sha1") | ||||
|         self.assertEqual( | ||||
|             encoded, "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8" | ||||
|         ) | ||||
|         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, "sha1") | ||||
|         # Blank passwords | ||||
|         blank_encoded = make_password("", "seasalt", "sha1") | ||||
|         self.assertTrue(blank_encoded.startswith("sha1$")) | ||||
|         self.assertTrue(is_password_usable(blank_encoded)) | ||||
|         self.assertTrue(check_password("", blank_encoded)) | ||||
|         self.assertFalse(check_password(" ", blank_encoded)) | ||||
|         # Salt entropy check. | ||||
|         hasher = get_hasher("sha1") | ||||
|         encoded_weak_salt = make_password("lètmein", "iodizedsalt", "sha1") | ||||
|         encoded_strong_salt = make_password("lètmein", hasher.salt(), "sha1") | ||||
|         self.assertIs(hasher.must_update(encoded_weak_salt), True) | ||||
|         self.assertIs(hasher.must_update(encoded_strong_salt), False) | ||||
|  | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"] | ||||
|     ) | ||||
|     def test_sha1_deprecation_warning(self): | ||||
|         msg = "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated." | ||||
|         with self.assertRaisesMessage(RemovedInDjango51Warning, msg): | ||||
|             get_hasher("sha1") | ||||
|  | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"] | ||||
|     ) | ||||
| @@ -160,85 +125,6 @@ class TestUtilsHashPass(SimpleTestCase): | ||||
|         self.assertIs(hasher.must_update(encoded_weak_salt), True) | ||||
|         self.assertIs(hasher.must_update(encoded_strong_salt), False) | ||||
|  | ||||
|     @ignore_warnings(category=RemovedInDjango51Warning) | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] | ||||
|     ) | ||||
|     def test_unsalted_md5(self): | ||||
|         encoded = make_password("lètmein", "", "unsalted_md5") | ||||
|         self.assertEqual(encoded, "88a434c88cca4e900f7874cd98123f43") | ||||
|         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_md5") | ||||
|         # Alternate unsalted syntax | ||||
|         alt_encoded = "md5$$%s" % encoded | ||||
|         self.assertTrue(is_password_usable(alt_encoded)) | ||||
|         self.assertTrue(check_password("lètmein", alt_encoded)) | ||||
|         self.assertFalse(check_password("lètmeinz", alt_encoded)) | ||||
|         # Blank passwords | ||||
|         blank_encoded = make_password("", "", "unsalted_md5") | ||||
|         self.assertTrue(is_password_usable(blank_encoded)) | ||||
|         self.assertTrue(check_password("", blank_encoded)) | ||||
|         self.assertFalse(check_password(" ", blank_encoded)) | ||||
|  | ||||
|     @ignore_warnings(category=RemovedInDjango51Warning) | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] | ||||
|     ) | ||||
|     def test_unsalted_md5_encode_invalid_salt(self): | ||||
|         hasher = get_hasher("unsalted_md5") | ||||
|         msg = "salt must be empty." | ||||
|         with self.assertRaisesMessage(ValueError, msg): | ||||
|             hasher.encode("password", salt="salt") | ||||
|  | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] | ||||
|     ) | ||||
|     def test_unsalted_md5_deprecation_warning(self): | ||||
|         msg = "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated." | ||||
|         with self.assertRaisesMessage(RemovedInDjango51Warning, msg): | ||||
|             get_hasher("unsalted_md5") | ||||
|  | ||||
|     @ignore_warnings(category=RemovedInDjango51Warning) | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] | ||||
|     ) | ||||
|     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)) | ||||
|         # Blank passwords | ||||
|         blank_encoded = make_password("", "", "unsalted_sha1") | ||||
|         self.assertTrue(blank_encoded.startswith("sha1$")) | ||||
|         self.assertTrue(is_password_usable(blank_encoded)) | ||||
|         self.assertTrue(check_password("", blank_encoded)) | ||||
|         self.assertFalse(check_password(" ", blank_encoded)) | ||||
|  | ||||
|     @ignore_warnings(category=RemovedInDjango51Warning) | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] | ||||
|     ) | ||||
|     def test_unsalted_sha1_encode_invalid_salt(self): | ||||
|         hasher = get_hasher("unsalted_sha1") | ||||
|         msg = "salt must be empty." | ||||
|         with self.assertRaisesMessage(ValueError, msg): | ||||
|             hasher.encode("password", salt="salt") | ||||
|  | ||||
|     @override_settings( | ||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] | ||||
|     ) | ||||
|     def test_unsalted_sha1_deprecation_warning(self): | ||||
|         msg = "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated." | ||||
|         with self.assertRaisesMessage(RemovedInDjango51Warning, msg): | ||||
|             get_hasher("unsalted_sha1") | ||||
|  | ||||
|     @skipUnless(bcrypt, "bcrypt not installed") | ||||
|     def test_bcrypt_sha256(self): | ||||
|         encoded = make_password("lètmein", hasher="bcrypt_sha256") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user