mirror of
https://github.com/django/django.git
synced 2025-06-05 11:39:13 +00:00
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,
|
get_random_string,
|
||||||
pbkdf2,
|
pbkdf2,
|
||||||
)
|
)
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.translation import gettext_noop as _
|
from django.utils.translation import gettext_noop as _
|
||||||
|
|
||||||
@ -641,57 +640,6 @@ class ScryptPasswordHasher(BasePasswordHasher):
|
|||||||
pass
|
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):
|
class MD5PasswordHasher(BasePasswordHasher):
|
||||||
"""
|
"""
|
||||||
The Salted MD5 password hashing algorithm (not recommended)
|
The Salted MD5 password hashing algorithm (not recommended)
|
||||||
@ -732,111 +680,3 @@ class MD5PasswordHasher(BasePasswordHasher):
|
|||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
def harden_runtime(self, password, encoded):
|
||||||
pass
|
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 model's ``Meta.index_together`` option is removed.
|
||||||
|
|
||||||
* The ``length_is`` template filter 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,
|
is_password_usable,
|
||||||
make_password,
|
make_password,
|
||||||
)
|
)
|
||||||
from django.test import SimpleTestCase, ignore_warnings
|
from django.test import SimpleTestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.utils.deprecation import RemovedInDjango51Warning
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import bcrypt
|
import bcrypt
|
||||||
@ -103,40 +102,6 @@ class TestUtilsHashPass(SimpleTestCase):
|
|||||||
self.assertIs(hasher.must_update(encoded_weak_salt), True)
|
self.assertIs(hasher.must_update(encoded_weak_salt), True)
|
||||||
self.assertIs(hasher.must_update(encoded_strong_salt), False)
|
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(
|
@override_settings(
|
||||||
PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"]
|
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_weak_salt), True)
|
||||||
self.assertIs(hasher.must_update(encoded_strong_salt), False)
|
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")
|
@skipUnless(bcrypt, "bcrypt not installed")
|
||||||
def test_bcrypt_sha256(self):
|
def test_bcrypt_sha256(self):
|
||||||
encoded = make_password("lètmein", hasher="bcrypt_sha256")
|
encoded = make_password("lètmein", hasher="bcrypt_sha256")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user