Skip to main content

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 (typically algorithm$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-cffi library.
  • Configuration: It uses specific parameters for time_cost (2), memory_cost (102400), and parallelism (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.

  1. Constant Time Comparison: The verify methods use django.utils.crypto.constant_time_compare to ensure that string comparisons don't leak information via early exits.
  2. Runtime Hardening: When a user provides an incorrect password for a hash that must_update, the system uses harden_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, or email. It uses difflib.SequenceMatcher to 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.