diff --git a/django/contrib/messages/storage/cookie.py b/django/contrib/messages/storage/cookie.py
index 30689dde3be..482ac5b27b1 100644
--- a/django/contrib/messages/storage/cookie.py
+++ b/django/contrib/messages/storage/cookie.py
@@ -4,7 +4,6 @@ from django.conf import settings
 from django.contrib.messages.storage.base import BaseStorage, Message
 from django.core import signing
 from django.http import SimpleCookie
-from django.utils.crypto import constant_time_compare, salted_hmac
 from django.utils.safestring import SafeData, mark_safe
 
 
@@ -139,18 +138,6 @@ class CookieStorage(BaseStorage):
         self._update_cookie(encoded_data, response)
         return unstored_messages
 
-    def _legacy_hash(self, value):
-        """
-        # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
-        Create an HMAC/SHA1 hash based on the value and the project setting's
-        SECRET_KEY, modified to make it unique for the present purpose.
-        """
-        # The class wide key salt is not reused here since older Django
-        # versions had it fixed and making it dynamic would break old hashes if
-        # self.key_salt is changed.
-        key_salt = 'django.contrib.messages'
-        return salted_hmac(key_salt, value).hexdigest()
-
     def _encode(self, messages, encode_empty=False):
         """
         Return an encoded version of the messages list which can be stored as
@@ -178,10 +165,7 @@ class CookieStorage(BaseStorage):
         # except (signing.BadSignature, json.JSONDecodeError):
         #     pass
         except signing.BadSignature:
-            # RemovedInDjango40Warning: when the deprecation ends, replace
-            # with:
-            #   decoded = None.
-            decoded = self._legacy_decode(data)
+            decoded = None
         except json.JSONDecodeError:
             decoded = self.signer.unsign(data)
 
@@ -195,12 +179,3 @@ class CookieStorage(BaseStorage):
         # with the data.
         self.used = True
         return None
-
-    def _legacy_decode(self, data):
-        # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
-        bits = data.split('$', 1)
-        if len(bits) == 2:
-            hash_, value = bits
-            if constant_time_compare(hash_, self._legacy_hash(value)):
-                return value
-        return None
diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt
index 34eb8acd959..c3ee1b06099 100644
--- a/docs/releases/4.0.txt
+++ b/docs/releases/4.0.txt
@@ -277,3 +277,6 @@ to remove usage of these features.
 * The ``django-admin.py`` entry point is removed.
 
 * The ``HttpRequest.is_ajax()`` method is removed.
+
+* Support for the pre-Django 3.1 encoding format of cookies values used by
+  ``django.contrib.messages.storage.cookie.CookieStorage`` is removed.
diff --git a/tests/messages_tests/test_cookie.py b/tests/messages_tests/test_cookie.py
index 9f82ce93e99..8df75fa9733 100644
--- a/tests/messages_tests/test_cookie.py
+++ b/tests/messages_tests/test_cookie.py
@@ -181,17 +181,6 @@ class CookieTests(BaseTests, SimpleTestCase):
         self.assertIsInstance(encode_decode(mark_safe("<b>Hello Django!</b>")), SafeData)
         self.assertNotIsInstance(encode_decode("<b>Hello Django!</b>"), SafeData)
 
-    def test_legacy_hash_decode(self):
-        # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
-        storage = self.storage_class(self.get_request())
-        messages = ['this', 'that']
-        # Encode/decode a message using the pre-Django 3.1 hash.
-        encoder = MessageEncoder()
-        value = encoder.encode(messages)
-        encoded_messages = '%s$%s' % (storage._legacy_hash(value), value)
-        decoded_messages = storage._decode(encoded_messages)
-        self.assertEqual(messages, decoded_messages)
-
     def test_legacy_encode_decode(self):
         # RemovedInDjango41Warning: pre-Django 3.2 encoded messages will be
         # invalid.