Fixed #32275 -- Added scrypt password hasher.
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
parent
65b880b726
commit
1783b3cb24
1
AUTHORS
1
AUTHORS
|
@ -82,6 +82,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Anssi Kääriäinen <akaariai@gmail.com>
|
Anssi Kääriäinen <akaariai@gmail.com>
|
||||||
ant9000@netwise.it
|
ant9000@netwise.it
|
||||||
Anthony Briggs <anthony.briggs@gmail.com>
|
Anthony Briggs <anthony.briggs@gmail.com>
|
||||||
|
Anthony Wright <ryow.college@gmail.com>
|
||||||
Anton Samarchyan <desecho@gmail.com>
|
Anton Samarchyan <desecho@gmail.com>
|
||||||
Antoni Aloy
|
Antoni Aloy
|
||||||
Antonio Cavedoni <http://cavedoni.com/>
|
Antonio Cavedoni <http://cavedoni.com/>
|
||||||
|
|
|
@ -520,6 +520,7 @@ PASSWORD_HASHERS = [
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = []
|
AUTH_PASSWORD_VALIDATORS = []
|
||||||
|
|
|
@ -517,6 +517,81 @@ class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
|
||||||
digest = None
|
digest = None
|
||||||
|
|
||||||
|
|
||||||
|
class ScryptPasswordHasher(BasePasswordHasher):
|
||||||
|
"""
|
||||||
|
Secure password hashing using the Scrypt algorithm.
|
||||||
|
"""
|
||||||
|
algorithm = 'scrypt'
|
||||||
|
block_size = 8
|
||||||
|
maxmem = 0
|
||||||
|
parallelism = 1
|
||||||
|
work_factor = 2 ** 14
|
||||||
|
|
||||||
|
def encode(self, password, salt, n=None, r=None, p=None):
|
||||||
|
self._check_encode_args(password, salt)
|
||||||
|
n = n or self.work_factor
|
||||||
|
r = r or self.block_size
|
||||||
|
p = p or self.parallelism
|
||||||
|
hash_ = hashlib.scrypt(
|
||||||
|
password.encode(),
|
||||||
|
salt=salt.encode(),
|
||||||
|
n=n,
|
||||||
|
r=r,
|
||||||
|
p=p,
|
||||||
|
maxmem=self.maxmem,
|
||||||
|
dklen=64,
|
||||||
|
)
|
||||||
|
hash_ = base64.b64encode(hash_).decode('ascii').strip()
|
||||||
|
return '%s$%d$%s$%d$%d$%s' % (self.algorithm, n, salt, r, p, hash_)
|
||||||
|
|
||||||
|
def decode(self, encoded):
|
||||||
|
algorithm, work_factor, salt, block_size, parallelism, hash_ = encoded.split('$', 6)
|
||||||
|
assert algorithm == self.algorithm
|
||||||
|
return {
|
||||||
|
'algorithm': algorithm,
|
||||||
|
'work_factor': int(work_factor),
|
||||||
|
'salt': salt,
|
||||||
|
'block_size': int(block_size),
|
||||||
|
'parallelism': int(parallelism),
|
||||||
|
'hash': hash_,
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify(self, password, encoded):
|
||||||
|
decoded = self.decode(encoded)
|
||||||
|
encoded_2 = self.encode(
|
||||||
|
password,
|
||||||
|
decoded['salt'],
|
||||||
|
decoded['work_factor'],
|
||||||
|
decoded['block_size'],
|
||||||
|
decoded['parallelism'],
|
||||||
|
)
|
||||||
|
return constant_time_compare(encoded, encoded_2)
|
||||||
|
|
||||||
|
def safe_summary(self, encoded):
|
||||||
|
decoded = self.decode(encoded)
|
||||||
|
return {
|
||||||
|
_('algorithm'): decoded['algorithm'],
|
||||||
|
_('work factor'): decoded['work_factor'],
|
||||||
|
_('block size'): decoded['block_size'],
|
||||||
|
_('parallelism'): decoded['parallelism'],
|
||||||
|
_('salt'): mask_hash(decoded['salt']),
|
||||||
|
_('hash'): mask_hash(decoded['hash']),
|
||||||
|
}
|
||||||
|
|
||||||
|
def must_update(self, encoded):
|
||||||
|
decoded = self.decode(encoded)
|
||||||
|
return (
|
||||||
|
decoded['work_factor'] != self.work_factor or
|
||||||
|
decoded['block_size'] != self.block_size or
|
||||||
|
decoded['parallelism'] != self.parallelism
|
||||||
|
)
|
||||||
|
|
||||||
|
def harden_runtime(self, password, encoded):
|
||||||
|
# The runtime for Scrypt is too complicated to implement a sensible
|
||||||
|
# hardening algorithm.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SHA1PasswordHasher(BasePasswordHasher):
|
class SHA1PasswordHasher(BasePasswordHasher):
|
||||||
"""
|
"""
|
||||||
The SHA1 password hashing algorithm (not recommended)
|
The SHA1 password hashing algorithm (not recommended)
|
||||||
|
|
|
@ -58,6 +58,13 @@ For example::
|
||||||
Functional unique constraints are added to models using the
|
Functional unique constraints are added to models using the
|
||||||
:attr:`Meta.constraints <django.db.models.Options.constraints>` option.
|
:attr:`Meta.constraints <django.db.models.Options.constraints>` option.
|
||||||
|
|
||||||
|
``scrypt`` password hasher
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The new :ref:`scrypt password hasher <scrypt-usage>` is more secure and
|
||||||
|
recommended over PBKDF2. However, it's not the default as it requires OpenSSL
|
||||||
|
1.1+ and more memory.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ The default for :setting:`PASSWORD_HASHERS` is::
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
This means that Django will use PBKDF2_ to store all passwords but will support
|
This means that Django will use PBKDF2_ to store all passwords but will support
|
||||||
|
@ -99,6 +100,7 @@ To use Argon2 as your default storage algorithm, do the following:
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
||||||
|
@ -129,6 +131,7 @@ To use Bcrypt as your default storage algorithm, do the following:
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
||||||
|
@ -137,6 +140,41 @@ To use Bcrypt as your default storage algorithm, do the following:
|
||||||
That's it -- now your Django install will use Bcrypt as the default storage
|
That's it -- now your Django install will use Bcrypt as the default storage
|
||||||
algorithm.
|
algorithm.
|
||||||
|
|
||||||
|
.. _scrypt-usage:
|
||||||
|
|
||||||
|
Using ``scrypt`` with Django
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
scrypt_ is similar to PBKDF2 and bcrypt in utilizing a set number of iterations
|
||||||
|
to slow down brute-force attacks. However, because PBKDF2 and bcrypt do not
|
||||||
|
require a lot of memory, attackers with sufficient resources can launch
|
||||||
|
large-scale parallel attacks in order to speed up the attacking process.
|
||||||
|
scrypt_ is specifically designed to use more memory compared to other
|
||||||
|
password-based key derivation functions in order to limit the amount of
|
||||||
|
parallelism an attacker can use, see :rfc:`7914` for more details.
|
||||||
|
|
||||||
|
To use scrypt_ as your default storage algorithm, do the following:
|
||||||
|
|
||||||
|
#. Modify :setting:`PASSWORD_HASHERS` to list ``ScryptPasswordHasher`` first.
|
||||||
|
That is, in your settings file::
|
||||||
|
|
||||||
|
PASSWORD_HASHERS = [
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
]
|
||||||
|
|
||||||
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
||||||
|
passwords <password-upgrades>`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
``scrypt`` requires OpenSSL 1.1+.
|
||||||
|
|
||||||
Increasing the salt entropy
|
Increasing the salt entropy
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
@ -197,6 +235,7 @@ algorithm:
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
That's it -- now your Django install will use more iterations when it
|
That's it -- now your Django install will use more iterations when it
|
||||||
|
@ -235,6 +274,32 @@ follows:
|
||||||
``memory_cost`` parameter differently from the value that Django uses. The
|
``memory_cost`` parameter differently from the value that Django uses. The
|
||||||
conversion is given by ``memory_cost == 2 ** memory_cost_commandline``.
|
conversion is given by ``memory_cost == 2 ** memory_cost_commandline``.
|
||||||
|
|
||||||
|
``scrypt``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
scrypt_ has four attributes that can be customized:
|
||||||
|
|
||||||
|
#. ``work_factor`` controls the number of iterations within the hash.
|
||||||
|
#. ``block_size``
|
||||||
|
#. ``parallelism`` controls how many threads will run in parallel.
|
||||||
|
#. ``maxmem`` limits the maximum size of memory that can be used during the
|
||||||
|
computation of the hash. Defaults to ``0``, which means the default
|
||||||
|
limitation from the OpenSSL library.
|
||||||
|
|
||||||
|
We've chosen reasonable defaults, but you may wish to tune it up or down,
|
||||||
|
depending on your security needs and available processing power.
|
||||||
|
|
||||||
|
.. admonition:: Estimating memory usage
|
||||||
|
|
||||||
|
The minimum memory requirement of scrypt_ is::
|
||||||
|
|
||||||
|
work_factor * 2 * block_size * 64
|
||||||
|
|
||||||
|
so you may need to tweak ``maxmem`` when changing the ``work_factor`` or
|
||||||
|
``block_size`` values.
|
||||||
|
|
||||||
.. _password-upgrades:
|
.. _password-upgrades:
|
||||||
|
|
||||||
Password upgrading
|
Password upgrading
|
||||||
|
@ -351,6 +416,7 @@ Include any other hashers that your site uses in this list.
|
||||||
.. _`bcrypt library`: https://pypi.org/project/bcrypt/
|
.. _`bcrypt library`: https://pypi.org/project/bcrypt/
|
||||||
.. _`argon2-cffi library`: https://pypi.org/project/argon2-cffi/
|
.. _`argon2-cffi library`: https://pypi.org/project/argon2-cffi/
|
||||||
.. _argon2: https://en.wikipedia.org/wiki/Argon2
|
.. _argon2: https://en.wikipedia.org/wiki/Argon2
|
||||||
|
.. _scrypt: https://en.wikipedia.org/wiki/Scrypt
|
||||||
.. _`Password Hashing Competition`: https://www.password-hashing.net/
|
.. _`Password Hashing Competition`: https://www.password-hashing.net/
|
||||||
|
|
||||||
.. _auth-included-hashers:
|
.. _auth-included-hashers:
|
||||||
|
@ -366,6 +432,7 @@ The full list of hashers included in Django is::
|
||||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
|
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
|
||||||
|
@ -380,6 +447,7 @@ The corresponding algorithm names are:
|
||||||
* ``argon2``
|
* ``argon2``
|
||||||
* ``bcrypt_sha256``
|
* ``bcrypt_sha256``
|
||||||
* ``bcrypt``
|
* ``bcrypt``
|
||||||
|
* ``scrypt``
|
||||||
* ``sha1``
|
* ``sha1``
|
||||||
* ``md5``
|
* ``md5``
|
||||||
* ``unsalted_sha1``
|
* ``unsalted_sha1``
|
||||||
|
|
|
@ -5,8 +5,8 @@ from django.contrib.auth.hashers import (
|
||||||
UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH,
|
UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH,
|
||||||
BasePasswordHasher, BCryptPasswordHasher, BCryptSHA256PasswordHasher,
|
BasePasswordHasher, BCryptPasswordHasher, BCryptSHA256PasswordHasher,
|
||||||
MD5PasswordHasher, PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher,
|
MD5PasswordHasher, PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher,
|
||||||
SHA1PasswordHasher, check_password, get_hasher, identify_hasher,
|
ScryptPasswordHasher, SHA1PasswordHasher, check_password, get_hasher,
|
||||||
is_password_usable, make_password,
|
identify_hasher, is_password_usable, make_password,
|
||||||
)
|
)
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
@ -480,6 +480,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
MD5PasswordHasher,
|
MD5PasswordHasher,
|
||||||
PBKDF2PasswordHasher,
|
PBKDF2PasswordHasher,
|
||||||
PBKDF2SHA1PasswordHasher,
|
PBKDF2SHA1PasswordHasher,
|
||||||
|
ScryptPasswordHasher,
|
||||||
SHA1PasswordHasher,
|
SHA1PasswordHasher,
|
||||||
]
|
]
|
||||||
msg = 'salt must be provided and cannot contain $.'
|
msg = 'salt must be provided and cannot contain $.'
|
||||||
|
@ -495,6 +496,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
MD5PasswordHasher,
|
MD5PasswordHasher,
|
||||||
PBKDF2PasswordHasher,
|
PBKDF2PasswordHasher,
|
||||||
PBKDF2SHA1PasswordHasher,
|
PBKDF2SHA1PasswordHasher,
|
||||||
|
ScryptPasswordHasher,
|
||||||
SHA1PasswordHasher,
|
SHA1PasswordHasher,
|
||||||
]
|
]
|
||||||
msg = 'password must be provided.'
|
msg = 'password must be provided.'
|
||||||
|
@ -662,3 +664,78 @@ class TestUtilsHashPassArgon2(SimpleTestCase):
|
||||||
self.assertTrue(state['upgraded'])
|
self.assertTrue(state['upgraded'])
|
||||||
finally:
|
finally:
|
||||||
setattr(hasher, attr, old_value)
|
setattr(hasher, attr, old_value)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
|
||||||
|
class TestUtilsHashPassScrypt(SimpleTestCase):
|
||||||
|
|
||||||
|
def test_scrypt(self):
|
||||||
|
encoded = make_password('lètmein', 'seasalt', 'scrypt')
|
||||||
|
self.assertEqual(
|
||||||
|
encoded,
|
||||||
|
'scrypt$16384$seasalt$8$1$Qj3+9PPyRjSJIebHnG81TMjsqtaIGxNQG/aEB/NY'
|
||||||
|
'afTJ7tibgfYz71m0ldQESkXFRkdVCBhhY8mx7rQwite/Pw=='
|
||||||
|
)
|
||||||
|
self.assertIs(is_password_usable(encoded), True)
|
||||||
|
self.assertIs(check_password('lètmein', encoded), True)
|
||||||
|
self.assertIs(check_password('lètmeinz', encoded), False)
|
||||||
|
self.assertEqual(identify_hasher(encoded).algorithm, "scrypt")
|
||||||
|
# Blank passwords.
|
||||||
|
blank_encoded = make_password('', 'seasalt', 'scrypt')
|
||||||
|
self.assertIs(blank_encoded.startswith('scrypt$'), True)
|
||||||
|
self.assertIs(is_password_usable(blank_encoded), True)
|
||||||
|
self.assertIs(check_password('', blank_encoded), True)
|
||||||
|
self.assertIs(check_password(' ', blank_encoded), False)
|
||||||
|
|
||||||
|
def test_scrypt_decode(self):
|
||||||
|
encoded = make_password('lètmein', 'seasalt', 'scrypt')
|
||||||
|
hasher = get_hasher('scrypt')
|
||||||
|
decoded = hasher.decode(encoded)
|
||||||
|
tests = [
|
||||||
|
('block_size', hasher.block_size),
|
||||||
|
('parallelism', hasher.parallelism),
|
||||||
|
('salt', 'seasalt'),
|
||||||
|
('work_factor', hasher.work_factor),
|
||||||
|
]
|
||||||
|
for key, excepted in tests:
|
||||||
|
with self.subTest(key=key):
|
||||||
|
self.assertEqual(decoded[key], excepted)
|
||||||
|
|
||||||
|
def _test_scrypt_upgrade(self, attr, summary_key, new_value):
|
||||||
|
hasher = get_hasher('scrypt')
|
||||||
|
self.assertEqual(hasher.algorithm, 'scrypt')
|
||||||
|
self.assertNotEqual(getattr(hasher, attr), new_value)
|
||||||
|
|
||||||
|
old_value = getattr(hasher, attr)
|
||||||
|
try:
|
||||||
|
# Generate hash with attr set to the new value.
|
||||||
|
setattr(hasher, attr, new_value)
|
||||||
|
encoded = make_password('lètmein', 'seasalt', 'scrypt')
|
||||||
|
attr_value = hasher.safe_summary(encoded)[summary_key]
|
||||||
|
self.assertEqual(attr_value, new_value)
|
||||||
|
|
||||||
|
state = {'upgraded': False}
|
||||||
|
|
||||||
|
def setter(password):
|
||||||
|
state['upgraded'] = True
|
||||||
|
|
||||||
|
# No update is triggered.
|
||||||
|
self.assertIs(check_password('lètmein', encoded, setter, 'scrypt'), True)
|
||||||
|
self.assertIs(state['upgraded'], False)
|
||||||
|
# Revert to the old value.
|
||||||
|
setattr(hasher, attr, old_value)
|
||||||
|
# Password is updated.
|
||||||
|
self.assertIs(check_password('lètmein', encoded, setter, 'scrypt'), True)
|
||||||
|
self.assertIs(state['upgraded'], True)
|
||||||
|
finally:
|
||||||
|
setattr(hasher, attr, old_value)
|
||||||
|
|
||||||
|
def test_scrypt_upgrade(self):
|
||||||
|
tests = [
|
||||||
|
('work_factor', 'work factor', 2 ** 11),
|
||||||
|
('block_size', 'block size', 10),
|
||||||
|
('parallelism', 'parallelism', 2),
|
||||||
|
]
|
||||||
|
for attr, summary_key, new_value in tests:
|
||||||
|
with self.subTest(attr=attr):
|
||||||
|
self._test_scrypt_upgrade(attr, summary_key, new_value)
|
||||||
|
|
Loading…
Reference in New Issue