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:
parent
6e5041f57c
commit
136ec9b62b
|
@ -206,6 +206,18 @@ class BasePasswordHasher:
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('subclasses of BasePasswordHasher must provide an encode() method')
|
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):
|
def safe_summary(self, encoded):
|
||||||
"""
|
"""
|
||||||
Return a summary of safe values.
|
Return a summary of safe values.
|
||||||
|
@ -252,31 +264,39 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
|
||||||
hash = base64.b64encode(hash).decode('ascii').strip()
|
hash = base64.b64encode(hash).decode('ascii').strip()
|
||||||
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
|
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def decode(self, 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):
|
|
||||||
algorithm, iterations, salt, hash = encoded.split('$', 3)
|
algorithm, iterations, salt, hash = encoded.split('$', 3)
|
||||||
assert algorithm == self.algorithm
|
assert algorithm == self.algorithm
|
||||||
return {
|
return {
|
||||||
_('algorithm'): algorithm,
|
'algorithm': algorithm,
|
||||||
_('iterations'): iterations,
|
'hash': hash,
|
||||||
_('salt'): mask_hash(salt),
|
'iterations': int(iterations),
|
||||||
_('hash'): mask_hash(hash),
|
'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):
|
def must_update(self, encoded):
|
||||||
algorithm, iterations, salt, hash = encoded.split('$', 3)
|
decoded = self.decode(encoded)
|
||||||
return int(iterations) != self.iterations
|
return decoded['iterations'] != self.iterations
|
||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
def harden_runtime(self, password, encoded):
|
||||||
algorithm, iterations, salt, hash = encoded.split('$', 3)
|
decoded = self.decode(encoded)
|
||||||
extra_iterations = self.iterations - int(iterations)
|
extra_iterations = self.iterations - decoded['iterations']
|
||||||
if extra_iterations > 0:
|
if extra_iterations > 0:
|
||||||
self.encode(password, salt, extra_iterations)
|
self.encode(password, decoded['salt'], extra_iterations)
|
||||||
|
|
||||||
|
|
||||||
class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
|
class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
|
||||||
|
@ -319,6 +339,23 @@ class Argon2PasswordHasher(BasePasswordHasher):
|
||||||
)
|
)
|
||||||
return self.algorithm + data.decode('ascii')
|
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):
|
def verify(self, password, encoded):
|
||||||
argon2 = self._load_library()
|
argon2 = self._load_library()
|
||||||
algorithm, rest = encoded.split('$', 1)
|
algorithm, rest = encoded.split('$', 1)
|
||||||
|
@ -329,18 +366,16 @@ class Argon2PasswordHasher(BasePasswordHasher):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
def safe_summary(self, encoded):
|
||||||
(algorithm, variety, version, time_cost, memory_cost, parallelism,
|
decoded = self.decode(encoded)
|
||||||
salt, data) = self._decode(encoded)
|
|
||||||
assert algorithm == self.algorithm
|
|
||||||
return {
|
return {
|
||||||
_('algorithm'): algorithm,
|
_('algorithm'): decoded['algorithm'],
|
||||||
_('variety'): variety,
|
_('variety'): decoded['variety'],
|
||||||
_('version'): version,
|
_('version'): decoded['version'],
|
||||||
_('memory cost'): memory_cost,
|
_('memory cost'): decoded['memory_cost'],
|
||||||
_('time cost'): time_cost,
|
_('time cost'): decoded['time_cost'],
|
||||||
_('parallelism'): parallelism,
|
_('parallelism'): decoded['parallelism'],
|
||||||
_('salt'): mask_hash(salt),
|
_('salt'): mask_hash(decoded['salt']),
|
||||||
_('hash'): mask_hash(data),
|
_('hash'): mask_hash(decoded['hash']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def must_update(self, encoded):
|
def must_update(self, encoded):
|
||||||
|
@ -372,22 +407,6 @@ class Argon2PasswordHasher(BasePasswordHasher):
|
||||||
parallelism=self.parallelism,
|
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):
|
class BCryptSHA256PasswordHasher(BasePasswordHasher):
|
||||||
"""
|
"""
|
||||||
|
@ -419,6 +438,17 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
|
||||||
data = bcrypt.hashpw(password, salt)
|
data = bcrypt.hashpw(password, salt)
|
||||||
return "%s$%s" % (self.algorithm, data.decode('ascii'))
|
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):
|
def verify(self, password, encoded):
|
||||||
algorithm, data = encoded.split('$', 1)
|
algorithm, data = encoded.split('$', 1)
|
||||||
assert algorithm == self.algorithm
|
assert algorithm == self.algorithm
|
||||||
|
@ -426,19 +456,17 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
|
||||||
return constant_time_compare(encoded, encoded_2)
|
return constant_time_compare(encoded, encoded_2)
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
def safe_summary(self, encoded):
|
||||||
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
|
decoded = self.decode(encoded)
|
||||||
assert algorithm == self.algorithm
|
|
||||||
salt, checksum = data[:22], data[22:]
|
|
||||||
return {
|
return {
|
||||||
_('algorithm'): algorithm,
|
_('algorithm'): decoded['algorithm'],
|
||||||
_('work factor'): work_factor,
|
_('work factor'): decoded['work_factor'],
|
||||||
_('salt'): mask_hash(salt),
|
_('salt'): mask_hash(decoded['salt']),
|
||||||
_('checksum'): mask_hash(checksum),
|
_('checksum'): mask_hash(decoded['checksum']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def must_update(self, encoded):
|
def must_update(self, encoded):
|
||||||
algorithm, empty, algostr, rounds, data = encoded.split('$', 4)
|
decoded = self.decode(encoded)
|
||||||
return int(rounds) != self.rounds
|
return decoded['work_factor'] != self.rounds
|
||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
def harden_runtime(self, password, encoded):
|
||||||
_, data = encoded.split('$', 1)
|
_, data = encoded.split('$', 1)
|
||||||
|
@ -480,19 +508,26 @@ class SHA1PasswordHasher(BasePasswordHasher):
|
||||||
hash = hashlib.sha1((salt + password).encode()).hexdigest()
|
hash = hashlib.sha1((salt + password).encode()).hexdigest()
|
||||||
return "%s$%s$%s" % (self.algorithm, salt, hash)
|
return "%s$%s$%s" % (self.algorithm, salt, hash)
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def decode(self, 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):
|
|
||||||
algorithm, salt, hash = encoded.split('$', 2)
|
algorithm, salt, hash = encoded.split('$', 2)
|
||||||
assert algorithm == self.algorithm
|
assert algorithm == self.algorithm
|
||||||
return {
|
return {
|
||||||
_('algorithm'): algorithm,
|
'algorithm': algorithm,
|
||||||
_('salt'): mask_hash(salt, show=2),
|
'hash': hash,
|
||||||
_('hash'): mask_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):
|
def harden_runtime(self, password, encoded):
|
||||||
|
@ -511,19 +546,26 @@ class MD5PasswordHasher(BasePasswordHasher):
|
||||||
hash = hashlib.md5((salt + password).encode()).hexdigest()
|
hash = hashlib.md5((salt + password).encode()).hexdigest()
|
||||||
return "%s$%s$%s" % (self.algorithm, salt, hash)
|
return "%s$%s$%s" % (self.algorithm, salt, hash)
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def decode(self, 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):
|
|
||||||
algorithm, salt, hash = encoded.split('$', 2)
|
algorithm, salt, hash = encoded.split('$', 2)
|
||||||
assert algorithm == self.algorithm
|
assert algorithm == self.algorithm
|
||||||
return {
|
return {
|
||||||
_('algorithm'): algorithm,
|
'algorithm': algorithm,
|
||||||
_('salt'): mask_hash(salt, show=2),
|
'hash': hash,
|
||||||
_('hash'): mask_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):
|
def harden_runtime(self, password, encoded):
|
||||||
|
@ -549,16 +591,23 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
|
||||||
hash = hashlib.sha1(password.encode()).hexdigest()
|
hash = hashlib.sha1(password.encode()).hexdigest()
|
||||||
return 'sha1$$%s' % hash
|
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):
|
def verify(self, password, encoded):
|
||||||
encoded_2 = self.encode(password, '')
|
encoded_2 = self.encode(password, '')
|
||||||
return constant_time_compare(encoded, encoded_2)
|
return constant_time_compare(encoded, encoded_2)
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
def safe_summary(self, encoded):
|
||||||
assert encoded.startswith('sha1$$')
|
decoded = self.decode(encoded)
|
||||||
hash = encoded[6:]
|
|
||||||
return {
|
return {
|
||||||
_('algorithm'): self.algorithm,
|
_('algorithm'): decoded['algorithm'],
|
||||||
_('hash'): mask_hash(hash),
|
_('hash'): mask_hash(decoded['hash']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
def harden_runtime(self, password, encoded):
|
||||||
|
@ -585,6 +634,13 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
|
||||||
assert salt == ''
|
assert salt == ''
|
||||||
return hashlib.md5(password.encode()).hexdigest()
|
return hashlib.md5(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
def decode(self, encoded):
|
||||||
|
return {
|
||||||
|
'algorithm': self.algorithm,
|
||||||
|
'hash': encoded,
|
||||||
|
'salt': None,
|
||||||
|
}
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
if len(encoded) == 37 and encoded.startswith('md5$$'):
|
if len(encoded) == 37 and encoded.startswith('md5$$'):
|
||||||
encoded = encoded[5:]
|
encoded = encoded[5:]
|
||||||
|
@ -592,9 +648,10 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
|
||||||
return constant_time_compare(encoded, encoded_2)
|
return constant_time_compare(encoded, encoded_2)
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
def safe_summary(self, encoded):
|
||||||
|
decoded = self.decode(encoded)
|
||||||
return {
|
return {
|
||||||
_('algorithm'): self.algorithm,
|
_('algorithm'): decoded['algorithm'],
|
||||||
_('hash'): mask_hash(encoded, show=3),
|
_('hash'): mask_hash(decoded['hash'], show=3),
|
||||||
}
|
}
|
||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
def harden_runtime(self, password, encoded):
|
||||||
|
@ -616,24 +673,32 @@ class CryptPasswordHasher(BasePasswordHasher):
|
||||||
def encode(self, password, salt):
|
def encode(self, password, salt):
|
||||||
crypt = self._load_library()
|
crypt = self._load_library()
|
||||||
assert len(salt) == 2
|
assert len(salt) == 2
|
||||||
data = crypt.crypt(password, salt)
|
hash = crypt.crypt(password, salt)
|
||||||
assert data is not None # A platform like OpenBSD with a dummy crypt module.
|
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
|
# 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):
|
def verify(self, password, encoded):
|
||||||
crypt = self._load_library()
|
crypt = self._load_library()
|
||||||
algorithm, salt, data = encoded.split('$', 2)
|
decoded = self.decode(encoded)
|
||||||
assert algorithm == self.algorithm
|
data = crypt.crypt(password, decoded['hash'])
|
||||||
return constant_time_compare(data, crypt.crypt(password, data))
|
return constant_time_compare(decoded['hash'], data)
|
||||||
|
|
||||||
def safe_summary(self, encoded):
|
def safe_summary(self, encoded):
|
||||||
algorithm, salt, data = encoded.split('$', 2)
|
decoded = self.decode(encoded)
|
||||||
assert algorithm == self.algorithm
|
|
||||||
return {
|
return {
|
||||||
_('algorithm'): algorithm,
|
_('algorithm'): decoded['algorithm'],
|
||||||
_('salt'): salt,
|
_('salt'): decoded['salt'],
|
||||||
_('hash'): mask_hash(data, show=3),
|
_('hash'): mask_hash(decoded['hash'], show=3),
|
||||||
}
|
}
|
||||||
|
|
||||||
def harden_runtime(self, password, encoded):
|
def harden_runtime(self, password, encoded):
|
||||||
|
|
|
@ -211,7 +211,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
hasher.rounds = 4
|
hasher.rounds = 4
|
||||||
encoded = make_password('letmein', hasher='bcrypt')
|
encoded = make_password('letmein', hasher='bcrypt')
|
||||||
rounds = hasher.safe_summary(encoded)['work factor']
|
rounds = hasher.safe_summary(encoded)['work factor']
|
||||||
self.assertEqual(rounds, '04')
|
self.assertEqual(rounds, 4)
|
||||||
|
|
||||||
state = {'upgraded': False}
|
state = {'upgraded': False}
|
||||||
|
|
||||||
|
@ -471,6 +471,11 @@ class BasePasswordHasherTests(SimpleTestCase):
|
||||||
with self.assertRaisesMessage(NotImplementedError, msg):
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
||||||
self.hasher.encode('password', 'salt')
|
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):
|
def test_harden_runtime(self):
|
||||||
msg = 'subclasses of BasePasswordHasher should provide a harden_runtime() method'
|
msg = 'subclasses of BasePasswordHasher should provide a harden_runtime() method'
|
||||||
with self.assertWarnsMessage(Warning, msg):
|
with self.assertWarnsMessage(Warning, msg):
|
||||||
|
|
Loading…
Reference in New Issue