summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2023-09-12 21:44:53 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-09-18 22:12:40 +0200
commit6e4e5523a8f40b63f3e74889266a7d638f6364dc (patch)
tree60e76e1f849a066015051aebebbbdaa9b6c795b6
parent14ef92fa9e87a77cd3642235387e6f683048046b (diff)
Refs #33691 -- Removed insecure password hashers per deprecation timeline.
-rw-r--r--django/contrib/auth/hashers.py160
-rw-r--r--docs/releases/5.1.txt4
-rw-r--r--tests/auth_tests/test_hashers.py116
3 files changed, 5 insertions, 275 deletions
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index b63904cd75..95b6e000bc 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 c6c4e2217d..9a0cd94f7e 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 e10992b25c..643f60ffc7 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")