diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index b63904cd75d..95b6e000bc4 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -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 diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index c6c4e2217d0..9a0cd94f7ee 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -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. diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py index e10992b25c6..643f60ffc78 100644 --- a/tests/auth_tests/test_hashers.py +++ b/tests/auth_tests/test_hashers.py @@ -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")