Password Hashing and Storage
Secure password storage in this project is managed through a pluggable architecture defined in django/contrib/auth/hashers.py. Instead of storing passwords in plain text, the system stores a string that includes the algorithm used, the salt, and the resulting hash. This design allows the system to support multiple hashing algorithms simultaneously and upgrade users to stronger algorithms automatically when they log in.
The PASSWORD_HASHERS Setting
The system determines which hashing algorithms are available and which one is the default via the PASSWORD_HASHERS setting. The first hasher in the list is used for all new passwords and for upgrading existing passwords.
A typical configuration might look like this:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.ScryptPasswordHasher',
]
Core Hashing API
The primary interface for password operations consists of two functions: make_password and check_password.
Creating Hashes
The make_password function takes a raw password and returns a formatted string. If the password is None, it returns an "unusable" password hash (starting with !), which cannot be matched by any raw string.
from django.contrib.auth.hashers import make_password
# Standard hashing
hashed = make_password("my_secret_password")
# Result format: <algorithm>$<iterations>$<salt>$<hash>
# e.g., 'pbkdf2_sha256$1500000$salt$hash'
Verifying and Upgrading
The check_password function verifies a raw password against a stored hash. It also supports an optional setter callback to automatically upgrade the stored hash if the algorithm or work factors have changed.
In django/contrib/auth/base_user.py, the AbstractBaseUser.check_password method implements this upgrade-on-login pattern:
def check_password(self, raw_password):
def setter(raw_password):
self.set_password(raw_password)
self._password = None
self.save(update_fields=["password"])
return check_password(raw_password, self.password, setter)
Supported Hashing Algorithms
PBKDF2 (Default)
The PBKDF2PasswordHasher is the default implementation. It uses PBKDF2 with HMAC-SHA256. It is highly resistant to brute-force attacks due to its high iteration count.
- Algorithm:
pbkdf2_sha256 - Default Iterations: 1,500,000
- File:
django/contrib/auth/hashers.py
Argon2
The Argon2PasswordHasher uses the Argon2 algorithm, which is designed to be memory-hard to resist GPU-based cracking. It requires the argon2-cffi library.
- Algorithm:
argon2 - Parameters:
time_cost=2,memory_cost=102400,parallelism=8
BCrypt
There are two BCrypt implementations. BCryptSHA256PasswordHasher is recommended because it hashes the password with SHA256 before passing it to BCrypt, bypassing BCrypt's 72-character password truncation limit.
- Algorithm:
bcrypt_sha256 - Rounds: 12
- Library:
bcrypt
Scrypt
The ScryptPasswordHasher provides another memory-hard alternative, utilizing the hashlib.scrypt implementation available in modern OpenSSL versions.
- Algorithm:
scrypt - Work Factor: 2^14 (16,384)
- Parallelism: 5
Security Mechanisms
Salt Generation
Every hasher inherits from BasePasswordHasher, which provides a salt() method. This method generates a cryptographically secure nonce using django.utils.crypto.get_random_string with a default entropy of 128 bits.
Timing Attack Protection
To prevent timing attacks, the system uses django.utils.crypto.constant_time_compare for all password comparisons.
Additionally, the harden_runtime method helps mitigate timing differences when a user's password uses an older, faster work factor. For example, PBKDF2PasswordHasher.harden_runtime calculates the difference between the stored iterations and the current default, then runs the remaining iterations to ensure the total computation time remains consistent.
def harden_runtime(self, password, encoded):
decoded = self.decode(encoded)
extra_iterations = self.iterations - decoded["iterations"]
if extra_iterations > 0:
self.encode(password, decoded["salt"], extra_iterations)
Implementing Custom Hashers
To implement a custom hashing algorithm, you must subclass BasePasswordHasher and override the core methods.
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import get_random_string
class MyCustomHasher(BasePasswordHasher):
algorithm = "custom_algo"
def encode(self, password, salt):
# Implementation of the hashing logic
# Must return a string formatted with '$' separators
pass
def verify(self, password, encoded):
# Implementation of verification logic
# Should use constant_time_compare
pass
def decode(self, encoded):
# Parse the encoded string into a dictionary of components
pass
def safe_summary(self, encoded):
# Return a dictionary of non-sensitive metadata for display
pass
Once defined, add the full import path of your custom class to the PASSWORD_HASHERS setting to enable it.