mirror of
https://github.com/django/django.git
synced 2025-08-21 09:19:12 +00:00
Fixed #36226 -- Accepted str or bytes for password and salt in password hashers.
Co-authored-by: Screamadelica <1621456391@sjtu.edu.cn>
This commit is contained in:
parent
e709301000
commit
78fac1b047
1
AUTHORS
1
AUTHORS
@ -903,6 +903,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Rob Nguyen <tienrobertnguyenn@gmail.com>
|
Rob Nguyen <tienrobertnguyenn@gmail.com>
|
||||||
Robin Munn <http://www.geekforgod.com/>
|
Robin Munn <http://www.geekforgod.com/>
|
||||||
Rodrigo Pinheiro Marques de Araújo <fenrrir@gmail.com>
|
Rodrigo Pinheiro Marques de Araújo <fenrrir@gmail.com>
|
||||||
|
Roel Delos Reyes <https://roelzkie.dev>
|
||||||
Rohith P R <https://rohithpr.com>
|
Rohith P R <https://rohithpr.com>
|
||||||
Romain Garrigues <romain.garrigues.cs@gmail.com>
|
Romain Garrigues <romain.garrigues.cs@gmail.com>
|
||||||
Ronnie van den Crommenacker
|
Ronnie van den Crommenacker
|
||||||
|
@ -16,6 +16,7 @@ from django.utils.crypto import (
|
|||||||
get_random_string,
|
get_random_string,
|
||||||
pbkdf2,
|
pbkdf2,
|
||||||
)
|
)
|
||||||
|
from django.utils.encoding import force_bytes, force_str
|
||||||
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 _
|
||||||
|
|
||||||
@ -252,7 +253,7 @@ class BasePasswordHasher:
|
|||||||
def _check_encode_args(self, password, salt):
|
def _check_encode_args(self, password, salt):
|
||||||
if password is None:
|
if password is None:
|
||||||
raise TypeError("password must be provided.")
|
raise TypeError("password must be provided.")
|
||||||
if not salt or "$" in salt:
|
if not salt or "$" in force_str(salt): # salt can be str or bytes.
|
||||||
raise ValueError("salt must be provided and cannot contain $.")
|
raise ValueError("salt must be provided and cannot contain $.")
|
||||||
|
|
||||||
def encode(self, password, salt):
|
def encode(self, password, salt):
|
||||||
@ -324,6 +325,8 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
|
|||||||
def encode(self, password, salt, iterations=None):
|
def encode(self, password, salt, iterations=None):
|
||||||
self._check_encode_args(password, salt)
|
self._check_encode_args(password, salt)
|
||||||
iterations = iterations or self.iterations
|
iterations = iterations or self.iterations
|
||||||
|
password = force_str(password)
|
||||||
|
salt = force_str(salt)
|
||||||
hash = pbkdf2(password, salt, iterations, digest=self.digest)
|
hash = pbkdf2(password, salt, iterations, digest=self.digest)
|
||||||
hash = base64.b64encode(hash).decode("ascii").strip()
|
hash = base64.b64encode(hash).decode("ascii").strip()
|
||||||
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
|
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
|
||||||
@ -396,8 +399,8 @@ class Argon2PasswordHasher(BasePasswordHasher):
|
|||||||
argon2 = self._load_library()
|
argon2 = self._load_library()
|
||||||
params = self.params()
|
params = self.params()
|
||||||
data = argon2.low_level.hash_secret(
|
data = argon2.low_level.hash_secret(
|
||||||
password.encode(),
|
force_bytes(password),
|
||||||
salt.encode(),
|
force_bytes(salt),
|
||||||
time_cost=params.time_cost,
|
time_cost=params.time_cost,
|
||||||
memory_cost=params.memory_cost,
|
memory_cost=params.memory_cost,
|
||||||
parallelism=params.parallelism,
|
parallelism=params.parallelism,
|
||||||
@ -499,7 +502,8 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
|
|||||||
|
|
||||||
def encode(self, password, salt):
|
def encode(self, password, salt):
|
||||||
bcrypt = self._load_library()
|
bcrypt = self._load_library()
|
||||||
password = password.encode()
|
password = force_bytes(password)
|
||||||
|
salt = force_bytes(salt)
|
||||||
# Hash the password prior to using bcrypt to prevent password
|
# Hash the password prior to using bcrypt to prevent password
|
||||||
# truncation as described in #20138.
|
# truncation as described in #20138.
|
||||||
if self.digest is not None:
|
if self.digest is not None:
|
||||||
@ -585,8 +589,8 @@ class ScryptPasswordHasher(BasePasswordHasher):
|
|||||||
r = r or self.block_size
|
r = r or self.block_size
|
||||||
p = p or self.parallelism
|
p = p or self.parallelism
|
||||||
hash_ = hashlib.scrypt(
|
hash_ = hashlib.scrypt(
|
||||||
password.encode(),
|
password=force_bytes(password),
|
||||||
salt=salt.encode(),
|
salt=force_bytes(salt),
|
||||||
n=n,
|
n=n,
|
||||||
r=r,
|
r=r,
|
||||||
p=p,
|
p=p,
|
||||||
@ -594,7 +598,7 @@ class ScryptPasswordHasher(BasePasswordHasher):
|
|||||||
dklen=64,
|
dklen=64,
|
||||||
)
|
)
|
||||||
hash_ = base64.b64encode(hash_).decode("ascii").strip()
|
hash_ = base64.b64encode(hash_).decode("ascii").strip()
|
||||||
return "%s$%d$%s$%d$%d$%s" % (self.algorithm, n, salt, r, p, hash_)
|
return "%s$%d$%s$%d$%d$%s" % (self.algorithm, n, force_str(salt), r, p, hash_)
|
||||||
|
|
||||||
def decode(self, encoded):
|
def decode(self, encoded):
|
||||||
algorithm, work_factor, salt, block_size, parallelism, hash_ = encoded.split(
|
algorithm, work_factor, salt, block_size, parallelism, hash_ = encoded.split(
|
||||||
@ -655,6 +659,8 @@ class MD5PasswordHasher(BasePasswordHasher):
|
|||||||
|
|
||||||
def encode(self, password, salt):
|
def encode(self, password, salt):
|
||||||
self._check_encode_args(password, salt)
|
self._check_encode_args(password, salt)
|
||||||
|
password = force_str(password)
|
||||||
|
salt = force_str(salt)
|
||||||
hash = hashlib.md5((salt + password).encode()).hexdigest()
|
hash = hashlib.md5((salt + password).encode()).hexdigest()
|
||||||
return "%s$%s$%s" % (self.algorithm, salt, hash)
|
return "%s$%s$%s" % (self.algorithm, salt, hash)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from django.conf.global_settings import PASSWORD_HASHERS
|
|||||||
from django.contrib.auth.hashers import (
|
from django.contrib.auth.hashers import (
|
||||||
UNUSABLE_PASSWORD_PREFIX,
|
UNUSABLE_PASSWORD_PREFIX,
|
||||||
UNUSABLE_PASSWORD_SUFFIX_LENGTH,
|
UNUSABLE_PASSWORD_SUFFIX_LENGTH,
|
||||||
|
Argon2PasswordHasher,
|
||||||
BasePasswordHasher,
|
BasePasswordHasher,
|
||||||
BCryptPasswordHasher,
|
BCryptPasswordHasher,
|
||||||
BCryptSHA256PasswordHasher,
|
BCryptSHA256PasswordHasher,
|
||||||
@ -520,6 +521,54 @@ class TestUtilsHashPass(SimpleTestCase):
|
|||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
hasher.encode("password", salt)
|
hasher.encode("password", salt)
|
||||||
|
|
||||||
|
def test_password_and_salt_in_str_and_bytes(self):
|
||||||
|
hasher_classes = [
|
||||||
|
MD5PasswordHasher,
|
||||||
|
PBKDF2PasswordHasher,
|
||||||
|
PBKDF2SHA1PasswordHasher,
|
||||||
|
ScryptPasswordHasher,
|
||||||
|
]
|
||||||
|
for hasher_class in hasher_classes:
|
||||||
|
hasher = hasher_class()
|
||||||
|
with self.subTest(hasher_class.__name__):
|
||||||
|
passwords = ["password", b"password"]
|
||||||
|
for password in passwords:
|
||||||
|
for salt in [hasher.salt(), hasher.salt().encode()]:
|
||||||
|
encoded = hasher.encode(password, salt)
|
||||||
|
for password_to_verify in passwords:
|
||||||
|
self.assertIs(
|
||||||
|
hasher.verify(password_to_verify, encoded), True
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnless(argon2, "argon2-cffi not installed")
|
||||||
|
def test_password_and_salt_in_str_and_bytes_argon2(self):
|
||||||
|
hasher = Argon2PasswordHasher()
|
||||||
|
passwords = ["password", b"password"]
|
||||||
|
for password in passwords:
|
||||||
|
for salt in [hasher.salt(), hasher.salt().encode()]:
|
||||||
|
encoded = hasher.encode(password, salt)
|
||||||
|
for password_to_verify in passwords:
|
||||||
|
self.assertIs(hasher.verify(password_to_verify, encoded), True)
|
||||||
|
|
||||||
|
@skipUnless(bcrypt, "bcrypt not installed")
|
||||||
|
def test_password_and_salt_in_str_and_bytes_bcrypt(self):
|
||||||
|
hasher_classes = [
|
||||||
|
BCryptPasswordHasher,
|
||||||
|
BCryptSHA256PasswordHasher,
|
||||||
|
]
|
||||||
|
for hasher_class in hasher_classes:
|
||||||
|
hasher = hasher_class()
|
||||||
|
with self.subTest(hasher_class.__name__):
|
||||||
|
passwords = ["password", b"password"]
|
||||||
|
for password in passwords:
|
||||||
|
salts = [hasher.salt().decode(), hasher.salt()]
|
||||||
|
for salt in salts:
|
||||||
|
encoded = hasher.encode(password, salt)
|
||||||
|
for password_to_verify in passwords:
|
||||||
|
self.assertIs(
|
||||||
|
hasher.verify(password_to_verify, encoded), True
|
||||||
|
)
|
||||||
|
|
||||||
def test_encode_password_required(self):
|
def test_encode_password_required(self):
|
||||||
hasher_classes = [
|
hasher_classes = [
|
||||||
MD5PasswordHasher,
|
MD5PasswordHasher,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user