Refs #31358 -- Added decode() to password hashers.

By convention a hasher which does not use a salt should populate the
decode dict with `None` rather than omit the dict key.

Co-Authored-By: Florian Apolloner <apollo13@users.noreply.github.com>
This commit is contained in:
Jon Moroney 2020-04-06 13:14:17 -07:00 committed by Mariusz Felisiak
parent 6e5041f57c
commit 136ec9b62b
2 changed files with 160 additions and 90 deletions

View File

@ -206,6 +206,18 @@ class BasePasswordHasher:
"""
raise NotImplementedError('subclasses of BasePasswordHasher must provide an encode() method')
def decode(self, encoded):
"""
Return a decoded database value.
The result is a dictionary and should contain `algorithm`, `hash`, and
`salt`. Extra keys can be algorithm specific like `iterations` or
`work_factor`.
"""
raise NotImplementedError(
'subclasses of BasePasswordHasher must provide a decode() method.'
)
def safe_summary(self, encoded):
"""
Return a summary of safe values.
@ -252,31 +264,39 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
hash = base64.b64encode(hash).decode('ascii').strip()
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
def verify(self, password, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, salt, int(iterations))
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
def decode(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
return {
_('algorithm'): algorithm,
_('iterations'): iterations,
_('salt'): mask_hash(salt),
_('hash'): mask_hash(hash),
'algorithm': algorithm,
'hash': hash,
'iterations': int(iterations),
'salt': salt,
}
def verify(self, password, encoded):
decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded['salt'], decoded['iterations'])
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
decoded = self.decode(encoded)
return {
_('algorithm'): decoded['algorithm'],
_('iterations'): decoded['iterations'],
_('salt'): mask_hash(decoded['salt']),
_('hash'): mask_hash(decoded['hash']),
}
def must_update(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
return int(iterations) != self.iterations
decoded = self.decode(encoded)
return decoded['iterations'] != self.iterations
def harden_runtime(self, password, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
extra_iterations = self.iterations - int(iterations)
decoded = self.decode(encoded)
extra_iterations = self.iterations - decoded['iterations']
if extra_iterations > 0:
self.encode(password, salt, extra_iterations)
self.encode(password, decoded['salt'], extra_iterations)
class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
@ -319,6 +339,23 @@ class Argon2PasswordHasher(BasePasswordHasher):
)
return self.algorithm + data.decode('ascii')
def decode(self, encoded):
argon2 = self._load_library()
algorithm, rest = encoded.split('$', 1)
assert algorithm == self.algorithm
params = argon2.extract_parameters('$' + rest)
variety, *_, salt, hash = rest.split('$')
return {
'algorithm': algorithm,
'hash': hash,
'memory_cost': params.memory_cost,
'parallelism': params.parallelism,
'salt': salt,
'time_cost': params.time_cost,
'variety': variety,
'version': params.version,
}
def verify(self, password, encoded):
argon2 = self._load_library()
algorithm, rest = encoded.split('$', 1)
@ -329,18 +366,16 @@ class Argon2PasswordHasher(BasePasswordHasher):
return False
def safe_summary(self, encoded):
(algorithm, variety, version, time_cost, memory_cost, parallelism,
salt, data) = self._decode(encoded)
assert algorithm == self.algorithm
decoded = self.decode(encoded)
return {
_('algorithm'): algorithm,
_('variety'): variety,
_('version'): version,
_('memory cost'): memory_cost,
_('time cost'): time_cost,
_('parallelism'): parallelism,
_('salt'): mask_hash(salt),
_('hash'): mask_hash(data),
_('algorithm'): decoded['algorithm'],
_('variety'): decoded['variety'],
_('version'): decoded['version'],
_('memory cost'): decoded['memory_cost'],
_('time cost'): decoded['time_cost'],
_('parallelism'): decoded['parallelism'],
_('salt'): mask_hash(decoded['salt']),
_('hash'): mask_hash(decoded['hash']),
}
def must_update(self, encoded):
@ -372,22 +407,6 @@ class Argon2PasswordHasher(BasePasswordHasher):
parallelism=self.parallelism,
)
def _decode(self, encoded):
"""
Split an encoded hash and return: (
algorithm, variety, version, time_cost, memory_cost,
parallelism, salt, data,
).
"""
argon2 = self._load_library()
algorithm, rest = encoded.split('$', 1)
params = argon2.extract_parameters('$' + rest)
variety, *_, salt, data = rest.split('$')
return (
algorithm, variety, params.version, params.time_cost,
params.memory_cost, params.parallelism, salt, data,
)
class BCryptSHA256PasswordHasher(BasePasswordHasher):
"""
@ -419,6 +438,17 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
data = bcrypt.hashpw(password, salt)
return "%s$%s" % (self.algorithm, data.decode('ascii'))
def decode(self, encoded):
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
assert algorithm == self.algorithm
return {
'algorithm': algorithm,
'algostr': algostr,
'checksum': data[22:],
'salt': data[:22],
'work_factor': int(work_factor),
}
def verify(self, password, encoded):
algorithm, data = encoded.split('$', 1)
assert algorithm == self.algorithm
@ -426,19 +456,17 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
assert algorithm == self.algorithm
salt, checksum = data[:22], data[22:]
decoded = self.decode(encoded)
return {
_('algorithm'): algorithm,
_('work factor'): work_factor,
_('salt'): mask_hash(salt),
_('checksum'): mask_hash(checksum),
_('algorithm'): decoded['algorithm'],
_('work factor'): decoded['work_factor'],
_('salt'): mask_hash(decoded['salt']),
_('checksum'): mask_hash(decoded['checksum']),
}
def must_update(self, encoded):
algorithm, empty, algostr, rounds, data = encoded.split('$', 4)
return int(rounds) != self.rounds
decoded = self.decode(encoded)
return decoded['work_factor'] != self.rounds
def harden_runtime(self, password, encoded):
_, data = encoded.split('$', 1)
@ -480,19 +508,26 @@ class SHA1PasswordHasher(BasePasswordHasher):
hash = hashlib.sha1((salt + password).encode()).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
def verify(self, password, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, salt)
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
def decode(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
return {
_('algorithm'): algorithm,
_('salt'): mask_hash(salt, show=2),
_('hash'): mask_hash(hash),
'algorithm': algorithm,
'hash': hash,
'salt': salt,
}
def verify(self, password, encoded):
decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded['salt'])
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
decoded = self.decode(encoded)
return {
_('algorithm'): decoded['algorithm'],
_('salt'): mask_hash(decoded['salt'], show=2),
_('hash'): mask_hash(decoded['hash']),
}
def harden_runtime(self, password, encoded):
@ -511,19 +546,26 @@ class MD5PasswordHasher(BasePasswordHasher):
hash = hashlib.md5((salt + password).encode()).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
def verify(self, password, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
encoded_2 = self.encode(password, salt)
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
def decode(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
return {
_('algorithm'): algorithm,
_('salt'): mask_hash(salt, show=2),
_('hash'): mask_hash(hash),
'algorithm': algorithm,
'hash': hash,
'salt': salt,
}
def verify(self, password, encoded):
decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded['salt'])
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
decoded = self.decode(encoded)
return {
_('algorithm'): decoded['algorithm'],
_('salt'): mask_hash(decoded['salt'], show=2),
_('hash'): mask_hash(decoded['hash']),
}
def harden_runtime(self, password, encoded):
@ -549,16 +591,23 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
hash = hashlib.sha1(password.encode()).hexdigest()
return 'sha1$$%s' % hash
def decode(self, encoded):
assert encoded.startswith('sha1$$')
return {
'algorithm': self.algorithm,
'hash': encoded[6:],
'salt': None,
}
def verify(self, password, encoded):
encoded_2 = self.encode(password, '')
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
assert encoded.startswith('sha1$$')
hash = encoded[6:]
decoded = self.decode(encoded)
return {
_('algorithm'): self.algorithm,
_('hash'): mask_hash(hash),
_('algorithm'): decoded['algorithm'],
_('hash'): mask_hash(decoded['hash']),
}
def harden_runtime(self, password, encoded):
@ -585,6 +634,13 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
assert salt == ''
return hashlib.md5(password.encode()).hexdigest()
def decode(self, encoded):
return {
'algorithm': self.algorithm,
'hash': encoded,
'salt': None,
}
def verify(self, password, encoded):
if len(encoded) == 37 and encoded.startswith('md5$$'):
encoded = encoded[5:]
@ -592,9 +648,10 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
decoded = self.decode(encoded)
return {
_('algorithm'): self.algorithm,
_('hash'): mask_hash(encoded, show=3),
_('algorithm'): decoded['algorithm'],
_('hash'): mask_hash(decoded['hash'], show=3),
}
def harden_runtime(self, password, encoded):
@ -616,24 +673,32 @@ class CryptPasswordHasher(BasePasswordHasher):
def encode(self, password, salt):
crypt = self._load_library()
assert len(salt) == 2
data = crypt.crypt(password, salt)
assert data is not None # A platform like OpenBSD with a dummy crypt module.
hash = crypt.crypt(password, salt)
assert hash is not None # A platform like OpenBSD with a dummy crypt module.
# we don't need to store the salt, but Django used to do this
return "%s$%s$%s" % (self.algorithm, '', data)
return '%s$%s$%s' % (self.algorithm, '', hash)
def decode(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
return {
'algorithm': algorithm,
'hash': hash,
'salt': salt,
}
def verify(self, password, encoded):
crypt = self._load_library()
algorithm, salt, data = encoded.split('$', 2)
assert algorithm == self.algorithm
return constant_time_compare(data, crypt.crypt(password, data))
decoded = self.decode(encoded)
data = crypt.crypt(password, decoded['hash'])
return constant_time_compare(decoded['hash'], data)
def safe_summary(self, encoded):
algorithm, salt, data = encoded.split('$', 2)
assert algorithm == self.algorithm
decoded = self.decode(encoded)
return {
_('algorithm'): algorithm,
_('salt'): salt,
_('hash'): mask_hash(data, show=3),
_('algorithm'): decoded['algorithm'],
_('salt'): decoded['salt'],
_('hash'): mask_hash(decoded['hash'], show=3),
}
def harden_runtime(self, password, encoded):

View File

@ -211,7 +211,7 @@ class TestUtilsHashPass(SimpleTestCase):
hasher.rounds = 4
encoded = make_password('letmein', hasher='bcrypt')
rounds = hasher.safe_summary(encoded)['work factor']
self.assertEqual(rounds, '04')
self.assertEqual(rounds, 4)
state = {'upgraded': False}
@ -471,6 +471,11 @@ class BasePasswordHasherTests(SimpleTestCase):
with self.assertRaisesMessage(NotImplementedError, msg):
self.hasher.encode('password', 'salt')
def test_decode(self):
msg = self.not_implemented_msg % 'a decode'
with self.assertRaisesMessage(NotImplementedError, msg):
self.hasher.decode('encoded')
def test_harden_runtime(self):
msg = 'subclasses of BasePasswordHasher should provide a harden_runtime() method'
with self.assertWarnsMessage(Warning, msg):