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:
parent
1ba0b22a7a
commit
a5033dbc58
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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=[
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
argon2-cffi == 16.0.0
|
argon2-cffi >= 16.1.0
|
||||||
bcrypt
|
bcrypt
|
||||||
docutils
|
docutils
|
||||||
geoip2
|
geoip2
|
||||||
|
|
Loading…
Reference in New Issue