Fixed #26033 -- Added Argon2 password hasher.

This commit is contained in:
Bas Westerbaan 2015-12-26 13:14:07 +01:00 committed by Tim Graham
parent 74670498e9
commit b4250ea04a
9 changed files with 215 additions and 1 deletions

View File

@ -500,6 +500,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 3
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]

View File

@ -297,6 +297,79 @@ class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
digest = hashlib.sha1
class Argon2PasswordHasher(BasePasswordHasher):
"""
Secure password hashing using the argon2 algorithm.
This is the winner of the Password Hashing Competition 2013-2015
(https://password-hashing.net). It requires the argon2-cffi library which
depends on native C code and might cause portability issues.
"""
algorithm = 'argon2'
library = 'argon2'
time_cost = 2
memory_cost = 512
parallelism = 2
def encode(self, password, salt):
argon2 = self._load_library()
data = argon2.low_level.hash_secret(
force_bytes(password),
force_bytes(salt),
time_cost=self.time_cost,
memory_cost=self.memory_cost,
parallelism=self.parallelism,
hash_len=argon2.DEFAULT_HASH_LENGTH,
type=argon2.low_level.Type.I,
)
return self.algorithm + data.decode('utf-8')
def verify(self, password, encoded):
argon2 = self._load_library()
algorithm, data = encoded.split('$', 1)
assert algorithm == self.algorithm
try:
return argon2.low_level.verify_secret(
force_bytes('$' + data),
force_bytes(password),
type=argon2.low_level.Type.I,
)
except argon2.exceptions.VerificationError:
return False
def safe_summary(self, encoded):
algorithm, variety, raw_pars, salt, data = encoded.split('$', 5)
pars = dict(bit.split('=', 1) for bit in raw_pars.split(','))
assert algorithm == self.algorithm
assert len(pars) == 3 and 't' in pars and 'm' in pars and 'p' in pars
return OrderedDict([
(_('algorithm'), algorithm),
(_('variety'), variety),
(_('memory cost'), int(pars['m'])),
(_('time cost'), int(pars['t'])),
(_('parallelism'), int(pars['p'])),
(_('salt'), mask_hash(salt)),
(_('hash'), mask_hash(data)),
])
def must_update(self, encoded):
algorithm, variety, raw_pars, salt, data = encoded.split('$', 5)
pars = dict([bit.split('=', 1) for bit in raw_pars.split(',')])
assert algorithm == self.algorithm
assert len(pars) == 3 and 't' in pars and 'm' in pars and 'p' in pars
return (
self.time_cost != int(pars['t']) or
self.memory_cost != int(pars['m']) or
self.parallelism != int(pars['p'])
)
def harden_runtime(self, password, encoded):
# The runtime for Argon2 is too complicated to implement a sensible
# hardening algorithm.
pass
class BCryptSHA256PasswordHasher(BasePasswordHasher):
"""
Secure password hashing using the bcrypt algorithm (recommended)

View File

@ -137,6 +137,7 @@ Running all the tests
If you want to run the full suite of tests, you'll need to install a number of
dependencies:
* argon2-cffi_ 16.0.0+
* bcrypt_
* docutils_
* enum34_ (Python 2 only)
@ -171,6 +172,7 @@ and install the Geospatial libraries</ref/contrib/gis/install/index>`.
Each of these dependencies is optional. If you're missing any of them, the
associated tests will be skipped.
.. _argon2-cffi: https://pypi.python.org/pypi/argon2_cffi
.. _bcrypt: https://pypi.python.org/pypi/bcrypt
.. _docutils: https://pypi.python.org/pypi/docutils
.. _enum34: https://pypi.python.org/pypi/enum34

View File

@ -2684,6 +2684,7 @@ Default::
[
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
@ -2702,6 +2703,8 @@ Default::
to strengthen the hashes in your database. If that's not feasible, add this
setting to your project and add back any hashers that you need.
Also, the ``Argon2PasswordHasher`` was added.
.. setting:: AUTH_PASSWORD_VALIDATORS
``AUTH_PASSWORD_VALIDATORS``

View File

@ -70,6 +70,10 @@ Minor features
:mod:`django.contrib.auth`
~~~~~~~~~~~~~~~~~~~~~~~~~~
* Added support for the :ref:`Argon2 password hash <argon2_usage>`. It's
recommended over PBKDF2, however, it's not the default as it requires a
third-party library.
* The default iteration count for the PBKDF2 password hasher has been increased
by 25%. This backwards compatible change will not affect users who have
subclassed ``django.contrib.auth.hashers.PBKDF2PasswordHasher`` to change the

View File

@ -60,16 +60,53 @@ The default for :setting:`PASSWORD_HASHERS` is::
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
This means that Django will use PBKDF2_ to store all passwords but will support
checking passwords stored with PBKDF2SHA1 and bcrypt_.
checking passwords stored with PBKDF2SHA1, argon2_, and bcrypt_.
The next few sections describe a couple of common ways advanced users may want
to modify this setting.
.. _argon2_usage:
Using Argon2 with Django
------------------------
.. versionadded:: 1.10
Argon2_ is the winner of the 2015 `Password Hashing Competition`_, a community
organized open competition to select a next generation hashing algorithm. It's
designed not to be easier to compute on custom hardware than it is to compute
on an ordinary CPU.
Argon2_ is not the default for Django because it requires a third-party
library. The Password Hashing Competition panel, however, recommends immediate
use of Argon2 rather than the other algorithms supported by Django.
To use Argon2 as your default storage algorithm, do the following:
1. Install the `argon2-cffi library`_. This can be done by running ``pip
install django[argon2]`` or by downloading the library and installing it
with ``python setup.py install``.
2. Modify :setting:`PASSWORD_HASHERS` to list ``Argon2PasswordHasher`` first.
That is, in your settings file, you'd put::
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
passwords <password-upgrades>`.
.. _bcrypt_usage:
Using ``bcrypt`` with Django
@ -94,6 +131,7 @@ To use Bcrypt as your default storage algorithm, do the following:
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
]
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
@ -132,6 +170,9 @@ algorithm.
Increasing the work factor
--------------------------
PBKDF2 and bcrypt
~~~~~~~~~~~~~~~~~
The PBKDF2 and bcrypt algorithms use a number of iterations or rounds of
hashing. This deliberately slows down attackers, making attacks against hashed
passwords harder. However, as computing power increases, the number of
@ -161,6 +202,7 @@ default PBKDF2 algorithm:
'myproject.hashers.MyPBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
@ -168,6 +210,28 @@ default PBKDF2 algorithm:
That's it -- now your Django install will use more iterations when it
stores passwords using PBKDF2.
Argon2
~~~~~~
Argon2 has three attributes that can be customized:
#. ``time_cost`` controls the number of iterations within the hash.
#. ``memory_cost`` controls the size of memory that must be used during the
computation of the hash.
#. ``parallelism`` controls how many CPUs the computation of the hash can be
parallelized on.
The default values of these attributes are probably fine for you. If you
determine that the password hash is too fast or too slow, you can tweak it as
follows:
#. Choose ``parallelism`` to be the number of threads you can
spare computing the hash.
#. Choose ``memory_cost`` to be the KiB of memory you can spare.
#. Adjust ``time_cost`` and measure the time hashing a password takes.
Pick a ``time_cost`` that takes an acceptable time for you.
If ``time_cost`` set to 1 is unacceptably slow, lower ``memory_cost``.
.. _password-upgrades:
Password upgrading
@ -286,6 +350,9 @@ Include any other hashers that your site uses in this list.
.. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
.. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
.. _`bcrypt library`: https://pypi.python.org/pypi/bcrypt/
.. _`argon2-cffi library`: https://pypi.python.org/pypi/argon2_cffi/
.. _argon2: https://en.wikipedia.org/wiki/Argon2
.. _`Password Hashing Competition`: https://password-hashing.net
.. _auth-included-hashers:
@ -297,6 +364,7 @@ The full list of hashers included in Django is::
[
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.SHA1PasswordHasher',
@ -310,6 +378,7 @@ The corresponding algorithm names are:
* ``pbkdf2_sha256``
* ``pbkdf2_sha1``
* ``argon2``
* ``bcrypt_sha256``
* ``bcrypt``
* ``sha1``

View File

@ -49,6 +49,7 @@ setup(
]},
extras_require={
"bcrypt": ["bcrypt"],
"argon2": ["argon2-cffi >= 16.0.0"],
},
zip_safe=False,
classifiers=[

View File

@ -25,6 +25,11 @@ try:
except ImportError:
bcrypt = None
try:
import argon2
except ImportError:
argon2 = None
class PBKDF2SingleIterationHasher(PBKDF2PasswordHasher):
iterations = 1
@ -434,3 +439,58 @@ class TestUtilsHashPass(SimpleTestCase):
with six.assertRaisesRegex(self, ValueError,
"Couldn't load 'PlainHasher' algorithm library: No module named '?plain'?"):
PlainHasher()._load_library()
@skipUnless(argon2, "argon2-cffi not installed")
@override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
class TestUtilsHashPassArgon2(SimpleTestCase):
def test_argon2(self):
encoded = make_password('lètmein', hasher='argon2')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(encoded.startswith('argon2$'))
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, 'argon2')
# Blank passwords
blank_encoded = make_password('', hasher='argon2')
self.assertTrue(blank_encoded.startswith('argon2$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_argon2_upgrade(self):
self._test_argon2_upgrade('time_cost', 'time cost', 1)
self._test_argon2_upgrade('memory_cost', 'memory cost', 16)
self._test_argon2_upgrade('parallelism', 'parallelism', 1)
def _test_argon2_upgrade(self, attr, summary_key, new_value):
hasher = get_hasher('argon2')
self.assertEqual('argon2', hasher.algorithm)
self.assertNotEqual(getattr(hasher, attr), new_value)
old_value = getattr(hasher, attr)
try:
# Generate hash with attr set to 1
setattr(hasher, attr, new_value)
encoded = make_password('letmein', hasher='argon2')
attr_value = hasher.safe_summary(encoded)[summary_key]
self.assertEqual(attr_value, new_value)
state = {'upgraded': False}
def setter(password):
state['upgraded'] = True
# Check that no upgrade is triggered.
self.assertTrue(check_password('letmein', encoded, setter, 'argon2'))
self.assertFalse(state['upgraded'])
# Revert to the old rounds count and ...
setattr(hasher, attr, old_value)
# ... check if the password would get updated to the new count.
self.assertTrue(check_password('letmein', encoded, setter, 'argon2'))
self.assertTrue(state['upgraded'])
finally:
setattr(hasher, attr, old_value)

View File

@ -1,3 +1,4 @@
argon2-cffi >= 16.0.0
bcrypt
docutils
geoip2