Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions django/contrib/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import hmac
import inspect
import re
import warnings
Expand All @@ -7,6 +6,7 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from django.utils.deprecation import RemovedInDjango61Warning
from django.utils.module_loading import import_string
from django.views.decorators.debug import sensitive_variables
Expand Down Expand Up @@ -175,7 +175,7 @@ def login(request, user, backend=None):
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash
and not hmac.compare_digest(
and not constant_time_compare(
request.session.get(HASH_SESSION_KEY, ""), session_auth_hash
)
):
Expand Down Expand Up @@ -217,7 +217,7 @@ async def alogin(request, user, backend=None):
if await request.session.ahas_key(SESSION_KEY):
if await _aget_user_session_key(request) != user.pk or (
session_auth_hash
and not hmac.compare_digest(
and not constant_time_compare(
await request.session.aget(HASH_SESSION_KEY, ""),
session_auth_hash,
)
Expand Down Expand Up @@ -323,15 +323,15 @@ def get_user(request):
session_hash_verified = False
else:
session_auth_hash = user.get_session_auth_hash()
session_hash_verified = hmac.compare_digest(
session_hash_verified = constant_time_compare(
session_hash, session_auth_hash
)
if not session_hash_verified:
# If the current secret does not verify the session, try
# with the fallback secrets and stop when a matching one is
# found.
if session_hash and any(
hmac.compare_digest(session_hash, fallback_auth_hash)
constant_time_compare(session_hash, fallback_auth_hash)
for fallback_auth_hash in user.get_session_auth_fallback_hash()
):
request.session.cycle_key()
Expand Down Expand Up @@ -364,15 +364,15 @@ async def aget_user(request):
session_hash_verified = False
else:
session_auth_hash = user.get_session_auth_hash()
session_hash_verified = hmac.compare_digest(
session_hash_verified = constant_time_compare(
session_hash, session_auth_hash
)
if not session_hash_verified:
# If the current secret does not verify the session, try
# with the fallback secrets and stop when a matching one is
# found.
if session_hash and any(
hmac.compare_digest(session_hash, fallback_auth_hash)
constant_time_compare(session_hash, fallback_auth_hash)
for fallback_auth_hash in user.get_session_auth_fallback_hash()
):
await request.session.acycle_key()
Expand Down
16 changes: 10 additions & 6 deletions django/contrib/auth/hashers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import binascii
import functools
import hashlib
import hmac
import importlib
import math
import warnings
Expand All @@ -13,7 +12,12 @@
from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed
from django.dispatch import receiver
from django.utils.crypto import RANDOM_STRING_CHARS, get_random_string, pbkdf2
from django.utils.crypto import (
RANDOM_STRING_CHARS,
constant_time_compare,
get_random_string,
pbkdf2,
)
from django.utils.encoding import force_bytes, force_str
from django.utils.module_loading import import_string
from django.utils.translation import gettext_noop as _
Expand Down Expand Up @@ -345,7 +349,7 @@ def decode(self, encoded):
def verify(self, password, encoded):
decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded["salt"], decoded["iterations"])
return hmac.compare_digest(encoded, encoded_2)
return constant_time_compare(encoded, encoded_2)

def safe_summary(self, encoded):
decoded = self.decode(encoded)
Expand Down Expand Up @@ -529,7 +533,7 @@ def verify(self, password, encoded):
algorithm, data = encoded.split("$", 1)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, data.encode("ascii"))
return hmac.compare_digest(encoded, encoded_2)
return constant_time_compare(encoded, encoded_2)

def safe_summary(self, encoded):
decoded = self.decode(encoded)
Expand Down Expand Up @@ -624,7 +628,7 @@ def verify(self, password, encoded):
decoded["block_size"],
decoded["parallelism"],
)
return hmac.compare_digest(encoded, encoded_2)
return constant_time_compare(encoded, encoded_2)

def safe_summary(self, encoded):
decoded = self.decode(encoded)
Expand Down Expand Up @@ -677,7 +681,7 @@ def decode(self, encoded):
def verify(self, password, encoded):
decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded["salt"])
return hmac.compare_digest(encoded, encoded_2)
return constant_time_compare(encoded, encoded_2)

def safe_summary(self, encoded):
decoded = self.decode(encoded)
Expand Down
5 changes: 2 additions & 3 deletions django/contrib/auth/tokens.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import hmac
from datetime import datetime

from django.conf import settings
from django.utils.crypto import salted_hmac
from django.utils.crypto import constant_time_compare, salted_hmac
from django.utils.http import base36_to_int, int_to_base36


Expand Down Expand Up @@ -68,7 +67,7 @@ def check_token(self, user, token):

# Check that the timestamp/uid has not been tampered with
for secret in [self.secret, *self.secret_fallbacks]:
if hmac.compare_digest(
if constant_time_compare(
self._make_token_with_timestamp(user, ts, secret),
token,
):
Expand Down
5 changes: 2 additions & 3 deletions django/core/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,12 @@

import base64
import datetime
import hmac
import json
import time
import zlib

from django.conf import settings
from django.utils.crypto import salted_hmac
from django.utils.crypto import constant_time_compare, salted_hmac
from django.utils.encoding import force_bytes
from django.utils.module_loading import import_string
from django.utils.regex_helper import _lazy_re_compile
Expand Down Expand Up @@ -210,7 +209,7 @@ def unsign(self, signed_value):
raise BadSignature('No "%s" found in value' % self.sep)
value, sig = signed_value.rsplit(self.sep, 1)
for key in [self.key, *self.fallback_keys]:
if hmac.compare_digest(sig, self.signature(value, key)):
if constant_time_compare(sig, self.signature(value, key)):
return value
raise BadSignature('Signature "%s" does not match' % sig)

Expand Down
5 changes: 2 additions & 3 deletions django/middleware/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
against request forgeries from other sites.
"""

import hmac
import logging
import string
from collections import defaultdict
Expand All @@ -16,7 +15,7 @@
from django.http import HttpHeaders, UnreadablePostError
from django.urls import get_callable
from django.utils.cache import patch_vary_headers
from django.utils.crypto import get_random_string
from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import cached_property
from django.utils.http import is_same_domain
Expand Down Expand Up @@ -155,7 +154,7 @@ def _does_token_match(request_csrf_token, csrf_secret):
if len(request_csrf_token) == CSRF_TOKEN_LENGTH:
request_csrf_token = _unmask_cipher_token(request_csrf_token)
assert len(request_csrf_token) == CSRF_SECRET_LENGTH
return hmac.compare_digest(request_csrf_token, csrf_secret)
return constant_time_compare(request_csrf_token, csrf_secret)


class RejectRequest(Exception):
Expand Down
9 changes: 1 addition & 8 deletions django/utils/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
import hashlib
import hmac
import secrets
import warnings

from django.conf import settings
from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.encoding import force_bytes


Expand Down Expand Up @@ -66,12 +64,7 @@ def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):

def constant_time_compare(val1, val2):
"""Return True if the two strings are equal, False otherwise."""
warnings.warn(
"constant_time_compare() is deprecated. Use hmac.compare_digest() instead.",
RemovedInDjango70Warning,
stacklevel=2,
)
return hmac.compare_digest(val1, val2)
return secrets.compare_digest(force_bytes(val1), force_bytes(val2))


def pbkdf2(password, salt, iterations, dklen=0, digest=None):
Expand Down
2 changes: 0 additions & 2 deletions docs/internals/deprecation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ details on these changes.
* The ``django.core.mail.forbid_multi_line_headers()`` and
``django.core.mail.message.sanitize_address()`` functions will be removed.

* The ``django.utils.crypto.constant_time_compare()`` function will be removed.

.. _deprecation-removed-in-6.1:

6.1
Expand Down
3 changes: 0 additions & 3 deletions docs/releases/6.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,6 @@ Miscellaneous
* The undocumented ``django.core.mail.forbid_multi_line_headers()`` and
``django.core.mail.message.sanitize_address()`` functions are deprecated.

* The ``django.utils.crypto.constant_time_compare()`` function is deprecated
because it is merely an alias of :py:func:`hmac.compare_digest`.

Features removed in 6.0
=======================

Expand Down
17 changes: 4 additions & 13 deletions tests/utils_tests/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,25 @@
import unittest

from django.test import SimpleTestCase
from django.test.utils import ignore_warnings
from django.utils.crypto import (
InvalidAlgorithm,
constant_time_compare,
pbkdf2,
salted_hmac,
)
from django.utils.deprecation import RemovedInDjango70Warning


class TestUtilsCryptoMisc(SimpleTestCase):
# RemovedInDjango70Warning.
@ignore_warnings(category=RemovedInDjango70Warning)
def test_constant_time_compare(self):
# It's hard to test for constant time, just test the result.
self.assertTrue(constant_time_compare(b"spam", b"spam"))
self.assertFalse(constant_time_compare(b"spam", b"eggs"))
self.assertTrue(constant_time_compare("spam", "spam"))
self.assertFalse(constant_time_compare("spam", "eggs"))

def test_constant_time_compare_deprecated(self):
msg = (
"constant_time_compare() is deprecated. "
"Use hmac.compare_digest() instead."
)
with self.assertWarnsMessage(RemovedInDjango70Warning, msg) as ctx:
constant_time_compare(b"spam", b"spam")
self.assertEqual(ctx.filename, __file__)
self.assertTrue(constant_time_compare(b"spam", "spam"))
self.assertFalse(constant_time_compare("spam", b"eggs"))
self.assertTrue(constant_time_compare("ありがとう", "ありがとう"))
self.assertFalse(constant_time_compare("ありがとう", "おはよう"))

def test_salted_hmac(self):
tests = [
Expand Down
Loading