Security

Password Storage and Cryptography

Proper password storage and cryptographic practices are fundamental to application security. Django provides robust built-in password hashing and cryptographic utilities, but understanding how to use them correctly is crucial for protecting user data.

Password Storage and Cryptography

Proper password storage and cryptographic practices are fundamental to application security. Django provides robust built-in password hashing and cryptographic utilities, but understanding how to use them correctly is crucial for protecting user data.

Password Hashing in Django

Django's Password System

# Django's password hashing system
from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.models import User

# How Django stores passwords
user = User.objects.create_user(
    username='testuser',
    password='mypassword123'
)

# Password is automatically hashed and stored
print(user.password)
# Output: pbkdf2_sha256$260000$randomsalt$hashedpassword

# Password format: algorithm$iterations$salt$hash
# - algorithm: pbkdf2_sha256 (default)
# - iterations: 260000 (configurable)
# - salt: random salt for this password
# - hash: the actual password hash

# Verifying passwords
is_valid = user.check_password('mypassword123')  # True
is_valid = user.check_password('wrongpassword')  # False

Password Hasher Configuration

# settings.py - Password hasher configuration
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',        # Default
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',    # Fallback
    'django.contrib.auth.hashers.Argon2PasswordHasher',        # Recommended
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',  # Alternative
    'django.contrib.auth.hashers.ScryptPasswordHasher',        # Modern option
]

# Recommended configuration for new projects
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',        # Primary
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',        # Fallback
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',    # Legacy support
]

# Argon2 configuration (requires argon2-cffi package)
# pip install argon2-cffi

Custom Password Hashers

# hashers.py - Custom password hasher
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import constant_time_compare
import hashlib
import secrets

class CustomPBKDF2Hasher(BasePasswordHasher):
    """Custom PBKDF2 hasher with enhanced security"""
    
    algorithm = "custom_pbkdf2"
    library = "hashlib"
    
    def encode(self, password, salt):
        """Encode password with custom parameters"""
        if not salt:
            salt = secrets.token_hex(16)  # 32-character hex salt
        
        # Use higher iteration count for better security
        iterations = 500000  # Increased from default 260000
        
        hash = hashlib.pbkdf2_hmac(
            'sha256',
            password.encode('utf-8'),
            salt.encode('utf-8'),
            iterations
        )
        
        hash_hex = hash.hex()
        return f"{self.algorithm}${iterations}${salt}${hash_hex}"
    
    def verify(self, password, encoded):
        """Verify password against encoded hash"""
        algorithm, iterations, salt, hash_hex = encoded.split('$', 3)
        
        assert algorithm == self.algorithm
        
        encoded_2 = self.encode(password, salt)
        return constant_time_compare(encoded, encoded_2)
    
    def safe_summary(self, encoded):
        """Return safe summary for admin display"""
        algorithm, iterations, salt, hash_hex = encoded.split('$', 3)
        
        return {
            'algorithm': algorithm,
            'iterations': iterations,
            'salt': salt[:6] + '...',  # Show only first 6 chars of salt
            'hash': hash_hex[:6] + '...',
        }
    
    def harden_runtime(self, password, encoded):
        """Harden against timing attacks"""
        pass
    
    def must_update(self, encoded):
        """Check if password needs to be updated"""
        algorithm, iterations, salt, hash_hex = encoded.split('$', 3)
        
        # Update if iterations are too low
        return int(iterations) < 400000

# Register custom hasher in settings
PASSWORD_HASHERS = [
    'myapp.hashers.CustomPBKDF2Hasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]

Password Validation

Built-in Password Validators

# settings.py - Password validation configuration
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
        'OPTIONS': {
            'user_attributes': ('username', 'first_name', 'last_name', 'email'),
            'max_similarity': 0.7,
        }
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 12,  # Increased from default 8
        }
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

Custom Password Validators

# validators.py - Custom password validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
import re
import requests
import hashlib

class ComplexityValidator:
    """Validate password complexity requirements"""
    
    def __init__(self, min_uppercase=1, min_lowercase=1, min_digits=1, min_special=1):
        self.min_uppercase = min_uppercase
        self.min_lowercase = min_lowercase
        self.min_digits = min_digits
        self.min_special = min_special
    
    def validate(self, password, user=None):
        """Validate password complexity"""
        errors = []
        
        # Count character types
        uppercase_count = sum(1 for c in password if c.isupper())
        lowercase_count = sum(1 for c in password if c.islower())
        digit_count = sum(1 for c in password if c.isdigit())
        special_count = sum(1 for c in password if not c.isalnum())
        
        # Check requirements
        if uppercase_count < self.min_uppercase:
            errors.append(f"Password must contain at least {self.min_uppercase} uppercase letter(s)")
        
        if lowercase_count < self.min_lowercase:
            errors.append(f"Password must contain at least {self.min_lowercase} lowercase letter(s)")
        
        if digit_count < self.min_digits:
            errors.append(f"Password must contain at least {self.min_digits} digit(s)")
        
        if special_count < self.min_special:
            errors.append(f"Password must contain at least {self.min_special} special character(s)")
        
        if errors:
            raise ValidationError(errors)
    
    def get_help_text(self):
        """Return help text for password requirements"""
        return _(
            f"Password must contain at least {self.min_uppercase} uppercase letter, "
            f"{self.min_lowercase} lowercase letter, {self.min_digits} digit, "
            f"and {self.min_special} special character."
        )

class BreachedPasswordValidator:
    """Check password against known breached passwords"""
    
    def __init__(self, check_online=False):
        self.check_online = check_online
    
    def validate(self, password, user=None):
        """Check if password has been breached"""
        
        # Check against local common passwords first
        if self.is_common_password(password):
            raise ValidationError(
                _("This password is too common and has been found in data breaches."),
                code='password_breached'
            )
        
        # Optional: Check against HaveIBeenPwned API
        if self.check_online and self.is_breached_online(password):
            raise ValidationError(
                _("This password has been found in data breaches and should not be used."),
                code='password_breached_online'
            )
    
    def is_common_password(self, password):
        """Check against local list of common passwords"""
        # This would check against a local database of common passwords
        common_passwords = {
            'password', '123456', 'password123', 'admin', 'qwerty',
            'letmein', 'welcome', 'monkey', '1234567890'
        }
        return password.lower() in common_passwords
    
    def is_breached_online(self, password):
        """Check password against HaveIBeenPwned API"""
        try:
            # Hash password with SHA-1
            sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
            
            # Use k-anonymity: send only first 5 characters
            prefix = sha1_hash[:5]
            suffix = sha1_hash[5:]
            
            # Query HaveIBeenPwned API
            response = requests.get(
                f'https://api.pwnedpasswords.com/range/{prefix}',
                timeout=5
            )
            
            if response.status_code == 200:
                # Check if our suffix appears in the response
                for line in response.text.splitlines():
                    hash_suffix, count = line.split(':')
                    if hash_suffix == suffix:
                        return True  # Password found in breach
            
            return False
            
        except Exception:
            # If API is unavailable, don't block password
            return False
    
    def get_help_text(self):
        return _("Password must not be found in known data breaches.")

class PatternValidator:
    """Validate against common password patterns"""
    
    def validate(self, password, user=None):
        """Check for common password patterns"""
        
        # Check for keyboard patterns
        keyboard_patterns = [
            'qwerty', 'asdf', 'zxcv', '1234', 'abcd'
        ]
        
        password_lower = password.lower()
        for pattern in keyboard_patterns:
            if pattern in password_lower:
                raise ValidationError(
                    _("Password contains a common keyboard pattern."),
                    code='keyboard_pattern'
                )
        
        # Check for repeated characters
        if re.search(r'(.)\1{2,}', password):  # 3+ repeated characters
            raise ValidationError(
                _("Password contains too many repeated characters."),
                code='repeated_characters'
            )
        
        # Check for simple patterns
        if re.match(r'^[a-zA-Z]+\d+$', password):  # Letters followed by numbers
            raise ValidationError(
                _("Password follows a predictable pattern (letters followed by numbers)."),
                code='predictable_pattern'
            )
    
    def get_help_text(self):
        return _("Password must not contain common patterns or repeated characters.")

# Register custom validators
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {'min_length': 12}
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
    {
        'NAME': 'myapp.validators.ComplexityValidator',
        'OPTIONS': {
            'min_uppercase': 1,
            'min_lowercase': 1,
            'min_digits': 1,
            'min_special': 1
        }
    },
    {
        'NAME': 'myapp.validators.BreachedPasswordValidator',
        'OPTIONS': {'check_online': True}
    },
    {
        'NAME': 'myapp.validators.PatternValidator',
    },
]

Cryptographic Utilities

Django's Cryptographic Functions

# Django's built-in cryptographic utilities
from django.utils.crypto import get_random_string, constant_time_compare
from django.core.signing import Signer, TimestampSigner
import secrets

# Generate secure random strings
random_token = get_random_string(32)  # 32-character random string
random_hex = secrets.token_hex(16)    # 32-character hex string
random_url_safe = secrets.token_urlsafe(32)  # URL-safe token

# Constant-time comparison (prevents timing attacks)
def verify_token(provided_token, stored_token):
    """Securely compare tokens"""
    return constant_time_compare(provided_token, stored_token)

# Digital signatures
signer = Signer()
signed_value = signer.sign('my_value')  # 'my_value:signature'
original_value = signer.unsign(signed_value)  # 'my_value'

# Timestamp signatures (with expiration)
timestamp_signer = TimestampSigner()
signed_value = timestamp_signer.sign('my_value')
original_value = timestamp_signer.unsign(signed_value, max_age=3600)  # 1 hour expiry

Encryption and Decryption

# encryption.py - Symmetric encryption utilities
from cryptography.fernet import Fernet
from django.conf import settings
import base64
import os

class FieldEncryption:
    """Utility class for field-level encryption"""
    
    def __init__(self):
        # Get encryption key from settings
        key = getattr(settings, 'FIELD_ENCRYPTION_KEY', None)
        if not key:
            # Generate a new key (store this securely!)
            key = Fernet.generate_key()
        
        if isinstance(key, str):
            key = key.encode()
        
        self.cipher = Fernet(key)
    
    def encrypt(self, plaintext):
        """Encrypt plaintext string"""
        if not plaintext:
            return plaintext
        
        if isinstance(plaintext, str):
            plaintext = plaintext.encode('utf-8')
        
        encrypted = self.cipher.encrypt(plaintext)
        return base64.urlsafe_b64encode(encrypted).decode('utf-8')
    
    def decrypt(self, ciphertext):
        """Decrypt ciphertext string"""
        if not ciphertext:
            return ciphertext
        
        try:
            encrypted_data = base64.urlsafe_b64decode(ciphertext.encode('utf-8'))
            decrypted = self.cipher.decrypt(encrypted_data)
            return decrypted.decode('utf-8')
        except Exception:
            # Return original if decryption fails (for migration scenarios)
            return ciphertext

# Encrypted model fields
from django.db import models

class EncryptedTextField(models.TextField):
    """Encrypted text field"""
    
    def __init__(self, *args, **kwargs):
        self.encryption = FieldEncryption()
        super().__init__(*args, **kwargs)
    
    def from_db_value(self, value, expression, connection):
        """Decrypt when loading from database"""
        if value is None:
            return value
        return self.encryption.decrypt(value)
    
    def to_python(self, value):
        """Convert to Python value"""
        if isinstance(value, str) or value is None:
            return value
        return str(value)
    
    def get_prep_value(self, value):
        """Encrypt when saving to database"""
        if value is None:
            return value
        return self.encryption.encrypt(str(value))

# Usage in models
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    
    # Regular fields
    bio = models.TextField(blank=True)
    
    # Encrypted fields
    ssn = EncryptedTextField(blank=True)  # Social Security Number
    phone = EncryptedTextField(blank=True)  # Phone number
    notes = EncryptedTextField(blank=True)  # Private notes

# settings.py - Encryption key configuration
# Generate key: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
FIELD_ENCRYPTION_KEY = os.environ.get('FIELD_ENCRYPTION_KEY')

if not FIELD_ENCRYPTION_KEY:
    raise ValueError("FIELD_ENCRYPTION_KEY must be set in environment variables")

Secure Token Generation

# tokens.py - Secure token generation and management
import secrets
import hashlib
import time
from django.utils.crypto import constant_time_compare
from django.core.cache import cache

class SecureTokenManager:
    """Manage secure tokens for various purposes"""
    
    def __init__(self, token_length=32):
        self.token_length = token_length
    
    def generate_token(self, purpose='general'):
        """Generate a secure random token"""
        return secrets.token_urlsafe(self.token_length)
    
    def generate_api_key(self, user_id, prefix='sk'):
        """Generate API key with user identification"""
        # Format: prefix_userid_randomtoken
        random_part = secrets.token_urlsafe(24)
        return f"{prefix}_{user_id}_{random_part}"
    
    def generate_reset_token(self, user):
        """Generate password reset token"""
        # Include user info and timestamp for validation
        timestamp = str(int(time.time()))
        user_data = f"{user.id}:{user.email}:{timestamp}"
        
        # Create token with user data hash
        token = secrets.token_urlsafe(32)
        token_hash = hashlib.sha256(f"{token}:{user_data}".encode()).hexdigest()
        
        # Store token hash in cache with expiration
        cache_key = f"reset_token:{user.id}"
        cache.set(cache_key, {
            'token_hash': token_hash,
            'timestamp': timestamp
        }, timeout=3600)  # 1 hour expiry
        
        return token
    
    def verify_reset_token(self, user, token):
        """Verify password reset token"""
        cache_key = f"reset_token:{user.id}"
        stored_data = cache.get(cache_key)
        
        if not stored_data:
            return False
        
        # Reconstruct user data
        user_data = f"{user.id}:{user.email}:{stored_data['timestamp']}"
        expected_hash = hashlib.sha256(f"{token}:{user_data}".encode()).hexdigest()
        
        # Verify token hash
        if constant_time_compare(stored_data['token_hash'], expected_hash):
            # Check expiration (1 hour)
            token_age = time.time() - int(stored_data['timestamp'])
            if token_age <= 3600:
                # Clean up used token
                cache.delete(cache_key)
                return True
        
        return False
    
    def generate_session_token(self, user, device_info=None):
        """Generate secure session token"""
        token = secrets.token_urlsafe(48)
        
        # Create token metadata
        metadata = {
            'user_id': user.id,
            'created_at': time.time(),
            'device_info': device_info or {},
            'token_hash': hashlib.sha256(token.encode()).hexdigest()
        }
        
        # Store in cache with session timeout
        cache_key = f"session_token:{token[:16]}"  # Use prefix for lookup
        cache.set(cache_key, metadata, timeout=86400)  # 24 hours
        
        return token
    
    def verify_session_token(self, token):
        """Verify session token and return user info"""
        cache_key = f"session_token:{token[:16]}"
        metadata = cache.get(cache_key)
        
        if not metadata:
            return None
        
        # Verify token hash
        expected_hash = hashlib.sha256(token.encode()).hexdigest()
        if not constant_time_compare(metadata['token_hash'], expected_hash):
            return None
        
        return metadata

# Usage in views
token_manager = SecureTokenManager()

def password_reset_request(request):
    """Handle password reset request"""
    if request.method == 'POST':
        email = request.POST.get('email')
        
        try:
            user = User.objects.get(email=email)
            
            # Generate reset token
            reset_token = token_manager.generate_reset_token(user)
            
            # Send reset email
            send_password_reset_email(user, reset_token)
            
            messages.success(request, "Password reset email sent")
            
        except User.DoesNotExist:
            # Don't reveal if email exists
            messages.success(request, "Password reset email sent")
    
    return render(request, 'password_reset.html')

def password_reset_confirm(request, token):
    """Handle password reset confirmation"""
    if request.method == 'POST':
        email = request.POST.get('email')
        new_password = request.POST.get('password')
        
        try:
            user = User.objects.get(email=email)
            
            # Verify reset token
            if token_manager.verify_reset_token(user, token):
                # Update password
                user.set_password(new_password)
                user.save()
                
                messages.success(request, "Password updated successfully")
                return redirect('login')
            else:
                messages.error(request, "Invalid or expired reset token")
                
        except User.DoesNotExist:
            messages.error(request, "Invalid reset request")
    
    return render(request, 'password_reset_confirm.html', {'token': token})

Secure Data Storage

Database Encryption

# Database-level encryption considerations
# settings.py - Database encryption configuration

# PostgreSQL with encryption
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'encrypted_db',
        'USER': 'db_user',
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': 'localhost',
        'PORT': '5432',
        'OPTIONS': {
            # Enable SSL
            'sslmode': 'require',
            'sslcert': '/path/to/client-cert.pem',
            'sslkey': '/path/to/client-key.pem',
            'sslrootcert': '/path/to/ca-cert.pem',
        },
    }
}

# Transparent Data Encryption (TDE) considerations:
# - Use database-level encryption for data at rest
# - Implement proper key management
# - Consider performance implications
# - Ensure backup encryption

File System Encryption

# File encryption utilities
import os
from cryptography.fernet import Fernet
from django.core.files.storage import FileSystemStorage

class EncryptedFileSystemStorage(FileSystemStorage):
    """File system storage with encryption"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Get encryption key
        key = os.environ.get('FILE_ENCRYPTION_KEY')
        if not key:
            key = Fernet.generate_key()
        
        if isinstance(key, str):
            key = key.encode()
        
        self.cipher = Fernet(key)
    
    def _save(self, name, content):
        """Encrypt file content before saving"""
        # Read content
        content.seek(0)
        file_content = content.read()
        
        # Encrypt content
        encrypted_content = self.cipher.encrypt(file_content)
        
        # Create new content file
        from django.core.files.base import ContentFile
        encrypted_file = ContentFile(encrypted_content)
        
        # Save encrypted file
        return super()._save(name, encrypted_file)
    
    def _open(self, name, mode='rb'):
        """Decrypt file content when opening"""
        # Open encrypted file
        encrypted_file = super()._open(name, mode)
        
        # Read and decrypt content
        encrypted_content = encrypted_file.read()
        decrypted_content = self.cipher.decrypt(encrypted_content)
        
        # Return decrypted content
        from django.core.files.base import ContentFile
        return ContentFile(decrypted_content)

# Usage in models
class SecureDocument(models.Model):
    title = models.CharField(max_length=200)
    file = models.FileField(
        upload_to='secure_docs/',
        storage=EncryptedFileSystemStorage()
    )
    uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

Key Management

Environment-Based Key Management

# Key management best practices
import os
from django.core.exceptions import ImproperlyConfigured

def get_encryption_key(key_name):
    """Securely retrieve encryption key from environment"""
    key = os.environ.get(key_name)
    
    if not key:
        raise ImproperlyConfigured(f"Encryption key {key_name} not found in environment")
    
    # Validate key format
    try:
        # For Fernet keys, validate base64 format
        import base64
        decoded = base64.urlsafe_b64decode(key)
        if len(decoded) != 32:  # Fernet keys are 32 bytes
            raise ValueError("Invalid key length")
    except Exception:
        raise ImproperlyConfigured(f"Invalid encryption key format for {key_name}")
    
    return key

# Key rotation utilities
class KeyRotationManager:
    """Manage encryption key rotation"""
    
    def __init__(self):
        self.current_key = get_encryption_key('CURRENT_ENCRYPTION_KEY')
        self.old_keys = self.load_old_keys()
    
    def load_old_keys(self):
        """Load old encryption keys for decryption"""
        old_keys = []
        
        # Load old keys from environment
        for i in range(1, 6):  # Support up to 5 old keys
            key_name = f'OLD_ENCRYPTION_KEY_{i}'
            key = os.environ.get(key_name)
            if key:
                old_keys.append(key)
        
        return old_keys
    
    def encrypt(self, plaintext):
        """Encrypt with current key"""
        cipher = Fernet(self.current_key.encode())
        return cipher.encrypt(plaintext.encode()).decode()
    
    def decrypt(self, ciphertext):
        """Decrypt with current or old keys"""
        # Try current key first
        try:
            cipher = Fernet(self.current_key.encode())
            return cipher.decrypt(ciphertext.encode()).decode()
        except Exception:
            pass
        
        # Try old keys
        for old_key in self.old_keys:
            try:
                cipher = Fernet(old_key.encode())
                return cipher.decrypt(ciphertext.encode()).decode()
            except Exception:
                continue
        
        raise ValueError("Unable to decrypt with any available key")
    
    def needs_reencryption(self, ciphertext):
        """Check if data needs re-encryption with current key"""
        try:
            # If current key can decrypt, no re-encryption needed
            cipher = Fernet(self.current_key.encode())
            cipher.decrypt(ciphertext.encode())
            return False
        except Exception:
            # If current key can't decrypt, re-encryption needed
            return True

# Key rotation management command
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    """Rotate encryption keys for encrypted fields"""
    
    help = 'Rotate encryption keys for encrypted model fields'
    
    def handle(self, *args, **options):
        key_manager = KeyRotationManager()
        
        # Find all models with encrypted fields
        encrypted_models = [UserProfile]  # Add your encrypted models
        
        for model_class in encrypted_models:
            self.rotate_model_keys(model_class, key_manager)
    
    def rotate_model_keys(self, model_class, key_manager):
        """Rotate keys for a specific model"""
        encrypted_fields = ['ssn', 'phone', 'notes']  # Your encrypted fields
        
        for obj in model_class.objects.all():
            updated = False
            
            for field_name in encrypted_fields:
                field_value = getattr(obj, field_name)
                
                if field_value and key_manager.needs_reencryption(field_value):
                    # Decrypt with old key and re-encrypt with new key
                    try:
                        decrypted = key_manager.decrypt(field_value)
                        reencrypted = key_manager.encrypt(decrypted)
                        setattr(obj, field_name, reencrypted)
                        updated = True
                    except Exception as e:
                        self.stdout.write(
                            self.style.ERROR(f'Failed to rotate key for {model_class.__name__} {obj.id}: {e}')
                        )
            
            if updated:
                obj.save()
                self.stdout.write(f'Rotated keys for {model_class.__name__} {obj.id}')

Testing Cryptographic Functions

Security Tests

# tests.py - Cryptographic function tests
from django.test import TestCase
from django.contrib.auth.models import User
from django.contrib.auth.hashers import check_password
import secrets

class PasswordSecurityTests(TestCase):
    """Test password security functions"""
    
    def test_password_hashing(self):
        """Test password hashing and verification"""
        password = 'test_password_123!'
        
        user = User.objects.create_user(
            username='testuser',
            password=password
        )
        
        # Password should be hashed
        self.assertNotEqual(user.password, password)
        self.assertTrue(user.password.startswith('pbkdf2_sha256$'))
        
        # Password verification should work
        self.assertTrue(user.check_password(password))
        self.assertFalse(user.check_password('wrong_password'))
    
    def test_password_validation(self):
        """Test custom password validators"""
        from django.contrib.auth.password_validation import validate_password
        from django.core.exceptions import ValidationError
        
        # Test weak passwords
        weak_passwords = [
            'password',      # Too common
            '12345678',      # All digits
            'abcdefgh',      # No complexity
            'Password',      # Missing digits/special chars
        ]
        
        for weak_password in weak_passwords:
            with self.assertRaises(ValidationError):
                validate_password(weak_password)
        
        # Test strong password
        strong_password = 'MyStr0ng!P@ssw0rd2023'
        try:
            validate_password(strong_password)
        except ValidationError:
            self.fail("Strong password should pass validation")
    
    def test_token_generation(self):
        """Test secure token generation"""
        token_manager = SecureTokenManager()
        
        # Test token uniqueness
        tokens = [token_manager.generate_token() for _ in range(100)]
        self.assertEqual(len(tokens), len(set(tokens)))  # All unique
        
        # Test token length
        token = token_manager.generate_token()
        self.assertGreaterEqual(len(token), 32)
        
        # Test API key format
        api_key = token_manager.generate_api_key(123)
        self.assertTrue(api_key.startswith('sk_123_'))
    
    def test_encryption_decryption(self):
        """Test field encryption and decryption"""
        encryption = FieldEncryption()
        
        # Test basic encryption/decryption
        plaintext = "Sensitive information"
        ciphertext = encryption.encrypt(plaintext)
        
        self.assertNotEqual(plaintext, ciphertext)
        self.assertEqual(plaintext, encryption.decrypt(ciphertext))
        
        # Test empty values
        self.assertEqual('', encryption.encrypt(''))
        self.assertIsNone(encryption.encrypt(None))
    
    def test_constant_time_comparison(self):
        """Test constant-time comparison function"""
        from django.utils.crypto import constant_time_compare
        
        # Test equal strings
        self.assertTrue(constant_time_compare('hello', 'hello'))
        
        # Test different strings
        self.assertFalse(constant_time_compare('hello', 'world'))
        
        # Test different lengths
        self.assertFalse(constant_time_compare('hello', 'hello world'))

class CryptographicUtilityTests(TestCase):
    """Test cryptographic utility functions"""
    
    def test_secure_random_generation(self):
        """Test secure random number generation"""
        # Test randomness
        random_values = [secrets.randbelow(1000000) for _ in range(1000)]
        
        # Check for reasonable distribution (not perfect randomness test)
        unique_values = len(set(random_values))
        self.assertGreater(unique_values, 900)  # Should be mostly unique
        
        # Test token generation
        tokens = [secrets.token_urlsafe(32) for _ in range(100)]
        self.assertEqual(len(tokens), len(set(tokens)))  # All unique
    
    def test_key_rotation(self):
        """Test encryption key rotation"""
        # This would test the key rotation functionality
        # Implementation depends on your specific key rotation setup
        pass

Best Practices Summary

Password Security

  • Use strong password hashing algorithms (Argon2, PBKDF2, bcrypt)
  • Implement comprehensive password validation
  • Check passwords against breach databases
  • Use secure password reset mechanisms

Cryptographic Practices

  • Use Django's built-in cryptographic utilities
  • Implement proper key management and rotation
  • Use constant-time comparison for sensitive operations
  • Generate cryptographically secure random values

Data Protection

  • Encrypt sensitive data at rest and in transit
  • Implement field-level encryption for PII
  • Use proper key derivation functions
  • Regular security audits and updates

Key Management

  • Store encryption keys securely (environment variables, key management services)
  • Implement key rotation procedures
  • Use different keys for different purposes
  • Monitor key usage and access

Next Steps

Now that you understand password storage and cryptography, let's explore the comprehensive secure deployment checklist to ensure your Django application is properly secured for production use.