Refs #33691 -- Removed insecure password hashers per deprecation timeline.

This commit is contained in:
Mariusz Felisiak 2023-09-12 21:44:53 +02:00
parent 14ef92fa9e
commit 6e4e5523a8
3 changed files with 5 additions and 275 deletions

View File

@ -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

View File

@ -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.

View File

@ -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")