summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBas Westerbaan <bas@westerbaan.name>2016-04-22 13:26:41 +0200
committerTim Graham <timograham@gmail.com>2016-04-25 21:17:53 -0400
commita5033dbc58d6e09d28b8abe3acda20b9c60e0b8e (patch)
tree7d8edefe51942a50bafbef0903c9e417de7858af
parent1ba0b22a7a262e63cb7152ef3ab513660156d135 (diff)
Refs #26033 -- Added password hasher support for Argon2 v1.3.
The previous version of Argon2 uses encoded hashes of the form: $argon2d$m=8,t=1,p=1$<salt>$<data> The new version of Argon2 adds its version into the hash: $argon2d$v=19$m=8,t=1,p=1$<salt>$<data> This lets Django handle both version properly.
-rw-r--r--django/contrib/auth/hashers.py56
-rw-r--r--docs/internals/contributing/writing-code/unit-tests.txt2
-rw-r--r--setup.py2
-rw-r--r--tests/auth_tests/test_hashers.py32
-rw-r--r--tests/requirements/base.txt2
5 files changed, 77 insertions, 17 deletions
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 6cd57f197a..8673b95cef 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -327,11 +327,11 @@ class Argon2PasswordHasher(BasePasswordHasher):
def verify(self, password, encoded):
argon2 = self._load_library()
- algorithm, data = encoded.split('$', 1)
+ algorithm, rest = encoded.split('$', 1)
assert algorithm == self.algorithm
try:
return argon2.low_level.verify_secret(
- force_bytes('$' + data),
+ force_bytes('$' + rest),
force_bytes(password),
type=argon2.low_level.Type.I,
)
@@ -339,29 +339,30 @@ class Argon2PasswordHasher(BasePasswordHasher):
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(','))
+ (algorithm, variety, version, time_cost, memory_cost, parallelism,
+ salt, data) = self._decode(encoded)
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'])),
+ (_('version'), version),
+ (_('memory cost'), memory_cost),
+ (_('time cost'), time_cost),
+ (_('parallelism'), parallelism),
(_('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(',')])
+ (algorithm, variety, version, time_cost, memory_cost, parallelism,
+ salt, data) = self._decode(encoded)
assert algorithm == self.algorithm
- assert len(pars) == 3 and 't' in pars and 'm' in pars and 'p' in pars
+ argon2 = self._load_library()
return (
- self.time_cost != int(pars['t']) or
- self.memory_cost != int(pars['m']) or
- self.parallelism != int(pars['p'])
+ argon2.low_level.ARGON2_VERSION != version or
+ self.time_cost != time_cost or
+ self.memory_cost != memory_cost or
+ self.parallelism != parallelism
)
def harden_runtime(self, password, encoded):
@@ -369,6 +370,33 @@ class Argon2PasswordHasher(BasePasswordHasher):
# hardening algorithm.
pass
+ def _decode(self, encoded):
+ """
+ Split an encoded hash and return: (
+ algorithm, variety, version, time_cost, memory_cost,
+ parallelism, salt, data,
+ ).
+ """
+ bits = encoded.split('$')
+ if len(bits) == 5:
+ # Argon2 < 1.3
+ algorithm, variety, raw_params, salt, data = bits
+ version = 0x10
+ else:
+ assert len(bits) == 6
+ algorithm, variety, raw_version, raw_params, salt, data = bits
+ assert raw_version.startswith('v=')
+ version = int(raw_version[len('v='):])
+ params = dict(bit.split('=', 1) for bit in raw_params.split(','))
+ assert len(params) == 3 and all(x in params for x in ('t', 'm', 'p'))
+ time_cost = int(params['t'])
+ memory_cost = int(params['m'])
+ parallelism = int(params['p'])
+ return (
+ algorithm, variety, version, time_cost, memory_cost, parallelism,
+ salt, data,
+ )
+
class BCryptSHA256PasswordHasher(BasePasswordHasher):
"""
diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
index b4ec5780cf..1eea23500d 100644
--- a/docs/internals/contributing/writing-code/unit-tests.txt
+++ b/docs/internals/contributing/writing-code/unit-tests.txt
@@ -142,7 +142,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+
+* argon2-cffi_ 16.1.0+
* bcrypt_
* docutils_
* enum34_ (Python 2 only)
diff --git a/setup.py b/setup.py
index 3657a2c471..4b18495bea 100644
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ setup(
]},
extras_require={
"bcrypt": ["bcrypt"],
- "argon2": ["argon2-cffi >= 16.0.0"],
+ "argon2": ["argon2-cffi >= 16.1.0"],
},
zip_safe=False,
classifiers=[
diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py
index 2525c2cae0..5b4ef7b378 100644
--- a/tests/auth_tests/test_hashers.py
+++ b/tests/auth_tests/test_hashers.py
@@ -457,12 +457,44 @@ class TestUtilsHashPassArgon2(SimpleTestCase):
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
+ # Old hashes without version attribute
+ encoded = (
+ 'argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO'
+ '4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg'
+ )
+ self.assertTrue(check_password('secret', encoded))
+ self.assertFalse(check_password('wrong', 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_version_upgrade(self):
+ hasher = get_hasher('argon2')
+ state = {'upgraded': False}
+ encoded = (
+ 'argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO'
+ '4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg'
+ )
+
+ def setter(password):
+ state['upgraded'] = True
+
+ old_m = hasher.memory_cost
+ old_t = hasher.time_cost
+ old_p = hasher.parallelism
+ try:
+ hasher.memory_cost = 8
+ hasher.time_cost = 1
+ hasher.parallelism = 1
+ self.assertTrue(check_password('secret', encoded, setter, 'argon2'))
+ self.assertTrue(state['upgraded'])
+ finally:
+ hasher.memory_cost = old_m
+ hasher.time_cost = old_t
+ hasher.parallelism = old_p
+
def _test_argon2_upgrade(self, attr, summary_key, new_value):
hasher = get_hasher('argon2')
self.assertEqual('argon2', hasher.algorithm)
diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt
index 4e996890bb..2cb272ff15 100644
--- a/tests/requirements/base.txt
+++ b/tests/requirements/base.txt
@@ -1,4 +1,4 @@
-argon2-cffi == 16.0.0
+argon2-cffi >= 16.1.0
bcrypt
docutils
geoip2