summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorJon Moroney <darakian@gmail.com>2020-06-24 19:28:07 -0700
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-01-14 11:20:28 +0100
commit76ae6ccf859bf677bfcb5b992f4c17f5af80ae9d (patch)
tree1f1b7bca42ce69a113c725af7dda4603333287a2 /django
parent6bd206e1ffcd6f2e16d6f615b6ba992448a149a8 (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.py35
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