summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBas Westerbaan <bas@westerbaan.name>2015-12-26 13:14:07 +0100
committerTim Graham <timograham@gmail.com>2016-03-08 11:22:18 -0500
commitb4250ea04a88f6c4fdf84dc8624baa1cf3e0f568 (patch)
treecd0a66c86a3405ccac86612ae986f190c5b5288b
parent74670498e902a0506e667cd21084c5e2eb71edfa (diff)
Fixed #26033 -- Added Argon2 password hasher.
-rw-r--r--django/conf/global_settings.py1
-rw-r--r--django/contrib/auth/hashers.py73
-rw-r--r--docs/internals/contributing/writing-code/unit-tests.txt2
-rw-r--r--docs/ref/settings.txt3
-rw-r--r--docs/releases/1.10.txt4
-rw-r--r--docs/topics/auth/passwords.txt71
-rw-r--r--setup.py1
-rw-r--r--tests/auth_tests/test_hashers.py60
-rw-r--r--tests/requirements/base.txt1
9 files changed, 215 insertions, 1 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 06b525d355..1f2df0f093 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -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',
]
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index ad0045267c..7658379871 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -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)
diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
index fb6fae1f72..37973261f4 100644
--- a/docs/internals/contributing/writing-code/unit-tests.txt
+++ b/docs/internals/contributing/writing-code/unit-tests.txt
@@ -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
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 9d94f53cfd..05276abae8 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -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``
diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt
index 47c964292f..2f617d3bea 100644
--- a/docs/releases/1.10.txt
+++ b/docs/releases/1.10.txt
@@ -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
diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
index 47d6486dd9..578d917cf4 100644
--- a/docs/topics/auth/passwords.txt
+++ b/docs/topics/auth/passwords.txt
@@ -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``
diff --git a/setup.py b/setup.py
index 8cbf4de8a7..3657a2c471 100644
--- a/setup.py
+++ b/setup.py
@@ -49,6 +49,7 @@ setup(
]},
extras_require={
"bcrypt": ["bcrypt"],
+ "argon2": ["argon2-cffi >= 16.0.0"],
},
zip_safe=False,
classifiers=[
diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py
index ecd3f276a9..a43c170ec1 100644
--- a/tests/auth_tests/test_hashers.py
+++ b/tests/auth_tests/test_hashers.py
@@ -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)
diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt
index 845aefbb87..4f702b5495 100644
--- a/tests/requirements/base.txt
+++ b/tests/requirements/base.txt
@@ -1,3 +1,4 @@
+argon2-cffi >= 16.0.0
bcrypt
docutils
geoip2