Fixed #31359 -- Deprecated get_random_string() calls without an explicit length.

This commit is contained in:
Claude Paroz 2020-03-11 09:47:43 +01:00 committed by Mariusz Felisiak
parent 5cc2c63f90
commit e663f695fb
6 changed files with 47 additions and 9 deletions

View File

@ -185,7 +185,8 @@ class BasePasswordHasher:
def salt(self): def salt(self):
"""Generate a cryptographically secure nonce salt in ASCII.""" """Generate a cryptographically secure nonce salt in ASCII."""
return get_random_string() # 12 returns a 71-bit value, log_2((26+26+10)^12) =~ 71 bits
return get_random_string(12)
def verify(self, password, encoded): def verify(self, password, encoded):
"""Check if the given password is correct.""" """Check if the given password is correct."""

View File

@ -341,7 +341,7 @@ class DatabaseCreation(BaseDatabaseCreation):
password = self._test_settings_get('PASSWORD') password = self._test_settings_get('PASSWORD')
if password is None and self._test_user_create(): if password is None and self._test_user_create():
# Oracle passwords are limited to 30 chars and can't contain symbols. # Oracle passwords are limited to 30 chars and can't contain symbols.
password = get_random_string(length=30) password = get_random_string(30)
return password return password
def _test_database_tblspace(self): def _test_database_tblspace(self):

View File

@ -4,8 +4,10 @@ Django's standard crypto functions and utilities.
import hashlib import hashlib
import hmac import hmac
import secrets import secrets
import warnings
from django.conf import settings from django.conf import settings
from django.utils.deprecation import RemovedInDjango40Warning
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -44,15 +46,31 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm='sha1'):
return hmac.new(key, msg=force_bytes(value), digestmod=hasher) return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
def get_random_string(length=12, NOT_PROVIDED = object() # RemovedInDjango40Warning.
allowed_chars='abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
# RemovedInDjango40Warning: when the deprecation ends, replace with:
# def get_random_string(self, length, allowed_chars='...'):
def get_random_string(length=NOT_PROVIDED, allowed_chars=(
'abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
)):
""" """
Return a securely generated random string. Return a securely generated random string.
The default length of 12 with the a-z, A-Z, 0-9 character set returns The bit length of the returned value can be calculated with the formula:
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits log_2(len(allowed_chars)^length)
For example, with default `allowed_chars` (26+26+10), this gives:
* length: 12, bit length =~ 71 bits
* length: 22, bit length =~ 131 bits
""" """
if length is NOT_PROVIDED:
warnings.warn(
'Not providing a length argument is deprecated.',
RemovedInDjango40Warning,
)
length = 12
return ''.join(secrets.choice(allowed_chars) for i in range(length)) return ''.join(secrets.choice(allowed_chars) for i in range(length))

View File

@ -61,6 +61,9 @@ details on these changes.
* The ``providing_args`` argument for ``django.dispatch.Signal`` will be * The ``providing_args`` argument for ``django.dispatch.Signal`` will be
removed. removed.
* The ``length`` argument for ``django.utils.crypto.get_random_string()`` will
be required.
See the :ref:`Django 3.1 release notes <deprecated-features-3.1>` for more See the :ref:`Django 3.1 release notes <deprecated-features-3.1>` for more
details on these changes. details on these changes.

View File

@ -554,6 +554,9 @@ Miscellaneous
argument as documentation, you can move the text to a code comment or argument as documentation, you can move the text to a code comment or
docstring. docstring.
* Calling ``django.utils.crypto.get_random_string()`` without a ``length``
argument is deprecated.
.. _removed-features-3.1: .. _removed-features-3.1:
Features removed in 3.1 Features removed in 3.1

View File

@ -1,10 +1,12 @@
import hashlib import hashlib
import unittest import unittest
from django.test import SimpleTestCase from django.test import SimpleTestCase, ignore_warnings
from django.utils.crypto import ( from django.utils.crypto import (
InvalidAlgorithm, constant_time_compare, pbkdf2, salted_hmac, InvalidAlgorithm, constant_time_compare, get_random_string, pbkdf2,
salted_hmac,
) )
from django.utils.deprecation import RemovedInDjango40Warning
class TestUtilsCryptoMisc(SimpleTestCase): class TestUtilsCryptoMisc(SimpleTestCase):
@ -183,3 +185,14 @@ class TestUtilsCryptoPBKDF2(unittest.TestCase):
def test_default_hmac_alg(self): def test_default_hmac_alg(self):
kwargs = {'password': b'password', 'salt': b'salt', 'iterations': 1, 'dklen': 20} kwargs = {'password': b'password', 'salt': b'salt', 'iterations': 1, 'dklen': 20}
self.assertEqual(pbkdf2(**kwargs), hashlib.pbkdf2_hmac(hash_name=hashlib.sha256().name, **kwargs)) self.assertEqual(pbkdf2(**kwargs), hashlib.pbkdf2_hmac(hash_name=hashlib.sha256().name, **kwargs))
class DeprecationTests(SimpleTestCase):
@ignore_warnings(category=RemovedInDjango40Warning)
def test_get_random_string(self):
self.assertEqual(len(get_random_string()), 12)
def test_get_random_string_warning(self):
msg = 'Not providing a length argument is deprecated.'
with self.assertRaisesMessage(RemovedInDjango40Warning, msg):
get_random_string()