diff options
| author | Jon Moroney <darakian@gmail.com> | 2020-06-24 19:28:07 -0700 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2021-01-14 11:20:28 +0100 |
| commit | 76ae6ccf859bf677bfcb5b992f4c17f5af80ae9d (patch) | |
| tree | 1f1b7bca42ce69a113c725af7dda4603333287a2 /django | |
| parent | 6bd206e1ffcd6f2e16d6f615b6ba992448a149a8 (diff) | |
Fixed #31358 -- Increased salt entropy of password hashers.
Co-authored-by: Florian Apolloner <florian@apolloner.eu>
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/auth/hashers.py | 35 |
1 files changed, 29 insertions, 6 deletions
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 7a751a694e..86ae7f42a8 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -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 |
