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.
This commit is contained in:
Bas Westerbaan 2016-04-22 13:26:41 +02:00 committed by Tim Graham
parent 1ba0b22a7a
commit a5033dbc58
5 changed files with 77 additions and 17 deletions

View File

@ -327,11 +327,11 @@ class Argon2PasswordHasher(BasePasswordHasher):
def verify(self, password, encoded): def verify(self, password, encoded):
argon2 = self._load_library() argon2 = self._load_library()
algorithm, data = encoded.split('$', 1) algorithm, rest = encoded.split('$', 1)
assert algorithm == self.algorithm assert algorithm == self.algorithm
try: try:
return argon2.low_level.verify_secret( return argon2.low_level.verify_secret(
force_bytes('$' + data), force_bytes('$' + rest),
force_bytes(password), force_bytes(password),
type=argon2.low_level.Type.I, type=argon2.low_level.Type.I,
) )
@ -339,29 +339,30 @@ class Argon2PasswordHasher(BasePasswordHasher):
return False return False
def safe_summary(self, encoded): def safe_summary(self, encoded):
algorithm, variety, raw_pars, salt, data = encoded.split('$', 5) (algorithm, variety, version, time_cost, memory_cost, parallelism,
pars = dict(bit.split('=', 1) for bit in raw_pars.split(',')) salt, data) = self._decode(encoded)
assert algorithm == self.algorithm assert algorithm == self.algorithm
assert len(pars) == 3 and 't' in pars and 'm' in pars and 'p' in pars
return OrderedDict([ return OrderedDict([
(_('algorithm'), algorithm), (_('algorithm'), algorithm),
(_('variety'), variety), (_('variety'), variety),
(_('memory cost'), int(pars['m'])), (_('version'), version),
(_('time cost'), int(pars['t'])), (_('memory cost'), memory_cost),
(_('parallelism'), int(pars['p'])), (_('time cost'), time_cost),
(_('parallelism'), parallelism),
(_('salt'), mask_hash(salt)), (_('salt'), mask_hash(salt)),
(_('hash'), mask_hash(data)), (_('hash'), mask_hash(data)),
]) ])
def must_update(self, encoded): def must_update(self, encoded):
algorithm, variety, raw_pars, salt, data = encoded.split('$', 5) (algorithm, variety, version, time_cost, memory_cost, parallelism,
pars = dict([bit.split('=', 1) for bit in raw_pars.split(',')]) salt, data) = self._decode(encoded)
assert algorithm == self.algorithm 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 ( return (
self.time_cost != int(pars['t']) or argon2.low_level.ARGON2_VERSION != version or
self.memory_cost != int(pars['m']) or self.time_cost != time_cost or
self.parallelism != int(pars['p']) self.memory_cost != memory_cost or
self.parallelism != parallelism
) )
def harden_runtime(self, password, encoded): def harden_runtime(self, password, encoded):
@ -369,6 +370,33 @@ class Argon2PasswordHasher(BasePasswordHasher):
# hardening algorithm. # hardening algorithm.
pass 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): class BCryptSHA256PasswordHasher(BasePasswordHasher):
""" """

View File

@ -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 If you want to run the full suite of tests, you'll need to install a number of
dependencies: dependencies:
* argon2-cffi_ 16.0.0+ * argon2-cffi_ 16.1.0+
* bcrypt_ * bcrypt_
* docutils_ * docutils_
* enum34_ (Python 2 only) * enum34_ (Python 2 only)

View File

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

View File

@ -457,12 +457,44 @@ class TestUtilsHashPassArgon2(SimpleTestCase):
self.assertTrue(is_password_usable(blank_encoded)) self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded)) self.assertTrue(check_password('', blank_encoded))
self.assertFalse(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): def test_argon2_upgrade(self):
self._test_argon2_upgrade('time_cost', 'time cost', 1) self._test_argon2_upgrade('time_cost', 'time cost', 1)
self._test_argon2_upgrade('memory_cost', 'memory cost', 16) self._test_argon2_upgrade('memory_cost', 'memory cost', 16)
self._test_argon2_upgrade('parallelism', 'parallelism', 1) 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): def _test_argon2_upgrade(self, attr, summary_key, new_value):
hasher = get_hasher('argon2') hasher = get_hasher('argon2')
self.assertEqual('argon2', hasher.algorithm) self.assertEqual('argon2', hasher.algorithm)

View File

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