Parcourir la source

Fixed #26033 -- Added Argon2 password hasher.

Bas Westerbaan il y a 9 ans
Parent
commit
b4250ea04a

+ 1 - 0
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',
 ]

+ 73 - 0
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)

+ 2 - 0
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

+ 3 - 0
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``

+ 4 - 0
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

+ 70 - 1
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``

+ 1 - 0
setup.py

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

+ 60 - 0
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)

+ 1 - 0
tests/requirements/base.txt

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