Skip to main content

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.