|
@@ -3,6 +3,7 @@ import binascii
|
|
|
import functools
|
|
|
import hashlib
|
|
|
import importlib
|
|
|
+import math
|
|
|
import warnings
|
|
|
|
|
|
from django.conf import settings
|
|
@@ -161,6 +162,11 @@ def mask_hash(hash, show=6, char="*"):
|
|
|
return masked
|
|
|
|
|
|
|
|
|
+def must_update_salt(salt, expected_entropy):
|
|
|
+ # Each character in the salt provides log_2(len(alphabet)) bits of entropy.
|
|
|
+ return len(salt) * math.log2(len(RANDOM_STRING_CHARS)) < expected_entropy
|
|
|
+
|
|
|
+
|
|
|
class BasePasswordHasher:
|
|
|
"""
|
|
|
Abstract base class for password hashers
|
|
@@ -172,6 +178,7 @@ class BasePasswordHasher:
|
|
|
"""
|
|
|
algorithm = None
|
|
|
library = None
|
|
|
+ salt_entropy = 128
|
|
|
|
|
|
def _load_library(self):
|
|
|
if self.library is not None:
|
|
@@ -189,9 +196,14 @@ class BasePasswordHasher:
|
|
|
self.__class__.__name__)
|
|
|
|
|
|
def salt(self):
|
|
|
- """Generate a cryptographically secure nonce salt in ASCII."""
|
|
|
- # 12 returns a 71-bit value, log_2(len(RANDOM_STRING_CHARS)^12) =~ 71 bits
|
|
|
- return get_random_string(12, RANDOM_STRING_CHARS)
|
|
|
+ """
|
|
|
+ Generate a cryptographically secure nonce salt in ASCII with an entropy
|
|
|
+ of at least `salt_entropy` bits.
|
|
|
+ """
|
|
|
+ # Each character in the salt provides
|
|
|
+ # log_2(len(alphabet)) bits of entropy.
|
|
|
+ char_count = math.ceil(self.salt_entropy / math.log2(len(RANDOM_STRING_CHARS)))
|
|
|
+ return get_random_string(char_count, allowed_chars=RANDOM_STRING_CHARS)
|
|
|
|
|
|
def verify(self, password, encoded):
|
|
|
"""Check if the given password is correct."""
|
|
@@ -290,7 +302,8 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
|
|
|
|
|
|
def must_update(self, encoded):
|
|
|
decoded = self.decode(encoded)
|
|
|
- return decoded['iterations'] != self.iterations
|
|
|
+ update_salt = must_update_salt(decoded['salt'], self.salt_entropy)
|
|
|
+ return (decoded['iterations'] != self.iterations) or update_salt
|
|
|
|
|
|
def harden_runtime(self, password, encoded):
|
|
|
decoded = self.decode(encoded)
|
|
@@ -383,12 +396,14 @@ class Argon2PasswordHasher(BasePasswordHasher):
|
|
|
}
|
|
|
|
|
|
def must_update(self, encoded):
|
|
|
- current_params = self.decode(encoded)['params']
|
|
|
+ decoded = self.decode(encoded)
|
|
|
+ current_params = decoded['params']
|
|
|
new_params = self.params()
|
|
|
# Set salt_len to the salt_len of the current parameters because salt
|
|
|
# is explicitly passed to argon2.
|
|
|
new_params.salt_len = current_params.salt_len
|
|
|
- return current_params != new_params
|
|
|
+ update_salt = must_update_salt(decoded['salt'], self.salt_entropy)
|
|
|
+ return (current_params != new_params) or update_salt
|
|
|
|
|
|
def harden_runtime(self, password, encoded):
|
|
|
# The runtime for Argon2 is too complicated to implement a sensible
|
|
@@ -531,6 +546,10 @@ class SHA1PasswordHasher(BasePasswordHasher):
|
|
|
_('hash'): mask_hash(decoded['hash']),
|
|
|
}
|
|
|
|
|
|
+ def must_update(self, encoded):
|
|
|
+ decoded = self.decode(encoded)
|
|
|
+ return must_update_salt(decoded['salt'], self.salt_entropy)
|
|
|
+
|
|
|
def harden_runtime(self, password, encoded):
|
|
|
pass
|
|
|
|
|
@@ -569,6 +588,10 @@ class MD5PasswordHasher(BasePasswordHasher):
|
|
|
_('hash'): mask_hash(decoded['hash']),
|
|
|
}
|
|
|
|
|
|
+ def must_update(self, encoded):
|
|
|
+ decoded = self.decode(encoded)
|
|
|
+ return must_update_salt(decoded['salt'], self.salt_entropy)
|
|
|
+
|
|
|
def harden_runtime(self, password, encoded):
|
|
|
pass
|
|
|
|