Password Storage and Hashing Algorithms
The password management system in this codebase is designed around the principle of "secure by default," providing a pluggable architecture that abstracts the complexities of cryptographic hashing while ensuring that user credentials evolve alongside security standards.
The Hashing Architecture
At the core of the system is a flexible framework defined in django/contrib/auth/hashers.py. Rather than hardcoding a single algorithm, the codebase utilizes a list of hasher classes defined in the PASSWORD_HASHERS setting.
The Base Interface
All hashing algorithms inherit from BasePasswordHasher. This abstract class establishes a consistent interface for the entire lifecycle of a password:
encode(password, salt): Transforms a raw password into a formatted string (typicallyalgorithm$iterations$salt$hash).verify(password, encoded): Validates a raw password against its stored hash.must_update(encoded): Determines if a stored hash is "stale" (e.g., if the default iteration count has increased).
High-Level API and Automatic Upgrading
Developers interact with passwords through two primary functions: make_password and check_password. A critical feature of this implementation is the automatic upgrading of password hashes. When check_password is called with a setter argument (usually the user object's set_password method), the system performs a check:
# From django/contrib/auth/hashers.py
def check_password(password, encoded, setter=None, preferred="default"):
is_correct, must_update = verify_password(password, encoded, preferred=preferred)
if setter and is_correct and must_update:
setter(password)
return is_correct
If the password is correct but the hasher indicates it must_update (because the algorithm has changed or the work factor is now higher), the password is re-hashed using the current default algorithm and saved immediately. This ensures that as security requirements increase, the database is transparently upgraded as users log in.
Hashing Algorithms and Tradeoffs
The codebase provides several implementations, each balancing security, performance, and external dependencies.
PBKDF2 (The Default)
PBKDF2PasswordHasher is the default choice because it is NIST-recommended and relies only on Python's standard library (hashlib). It uses HMAC-SHA256 and is configured with a high iteration count (defaulting to 1,500,000 in this version) to resist brute-force attacks.
Argon2 (The Modern Choice)
Argon2PasswordHasher implements the winner of the Password Hashing Competition. It is a "memory-hard" algorithm, designed to be resistant to GPU and ASIC-based cracking.
- Tradeoff: It requires the
argon2-cffilibrary. - Configuration: It uses specific parameters for
time_cost(2),memory_cost(102400), andparallelism(8).
Scrypt
ScryptPasswordHasher is another memory-hard alternative. Like Argon2, it is designed to make hardware-accelerated attacks expensive by requiring significant RAM. It uses a work_factor of 2**14 and a block_size of 8.
BCrypt and the SHA256 Variant
Standard BCrypt has a known limitation: it truncates passwords longer than 72 characters. To solve this, the codebase provides BCryptSHA256PasswordHasher.
# From django/contrib/auth/hashers.py
def encode(self, password, salt):
# ...
# Hash the password prior to using bcrypt to prevent password
# truncation as described in #20138.
if self.digest is not None:
password = binascii.hexlify(self.digest(password).digest())
data = bcrypt.hashpw(password, salt)
return "%s$%s" % (self.algorithm, data.decode("ascii"))
By pre-hashing the password with SHA256, the input to BCrypt is always a fixed-length hex string, effectively bypassing the truncation limit while maintaining BCrypt's strong adaptive hashing.
Mitigating Timing Attacks
The implementation is sensitive to timing attacks, where an attacker might infer information based on how long a verification takes.
- Constant Time Comparison: The
verifymethods usedjango.utils.crypto.constant_time_compareto ensure that string comparisons don't leak information via early exits. - Runtime Hardening: When a user provides an incorrect password for a hash that
must_update, the system usesharden_runtime. For PBKDF2, this method executes the "missing" iterations to ensure the response time matches what it would have been for a modern hash, preventing attackers from identifying accounts with older, weaker hashes.
Password Strength Validation
Beyond storage, the codebase enforces password quality through the django.contrib.auth.password_validation module. These validators are configured via the AUTH_PASSWORD_VALIDATORS setting.
Built-in Validators
- MinimumLengthValidator: A simple check against a
min_length(default 8). - UserAttributeSimilarityValidator: Prevents passwords from being too similar to the user's
username,first_name,last_name, oremail. It usesdifflib.SequenceMatcherto calculate a similarity ratio. - CommonPasswordValidator: Compares the password against a gzipped list of 20,000 common passwords (
common-passwords.txt.gz). This is significantly more effective than simple complexity rules (like requiring a symbol). - NumericPasswordValidator: Rejects passwords that are entirely digits, preventing users from using simple PIN-like passwords.
Validation Logic
The validate_password function iterates through all configured validators. If any validator fails, it raises a ValidationError containing all collected error messages, which can then be displayed in forms like UserCreationForm.