mirror of https://github.com/django/django.git
Refs #33691 -- Removed insecure password hashers per deprecation timeline.
This commit is contained in:
parent
14ef92fa9e
commit
6e4e5523a8
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue