I have an old database where user passwords were hashed with md5 without salt. Now I am converting the project into django and need to update passwords without asking users to log in.
I wrote this hasher:
from django.contrib.auth.hashers import PBKDF2PasswordHasher
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = 'pbkdf2_wrapped_md5'
def encode_md5_hash(self, md5_hash, salt):
return super().encode(md5_hash, salt)
and converting password like:
for data in old_user_data:
hasher = PBKDF2WrappedMD5PasswordHasher()
random_salt = get_random_string(length=8)
# data['password'] is e.g. '972131D979FF69F96DDFCC7AE3769B31'
user.password = hasher.encode_md5_hash(data['password'], random_salt)
but I can't login with my test-user.
Answer
To solve the issue with logging in users whose passwords were hashed using MD5 (without salt), and then updating those passwords in Django while keeping compatibility with your current hashing method, there are a few things to consider.
The primary problem is how you're trying to convert the MD5 hash into a PBKDF2 hash. You can't simply pass an MD5 hash directly into the PBKDF2 password hashing algorithm because PBKDF2 is a different cryptographic mechanism designed for security, and it doesn't directly work with raw MD5 hashes. So, to convert the existing MD5 hashes into the PBKDF2 format, we need a strategy that ensures compatibility with Django's authentication system.
Solution Overview:
- When a user logs in, check if their password is hashed using MD5 (without salt).
- If it is, take the MD5 hash, and rehash it with a secure algorithm (like PBKDF2) and store the new hashed password in the database.
- Ensure the old MD5 password hash is updated with a new PBKDF2 hash without requiring users to re-enter their passwords.
Updated Approach
- Define a custom password hasher: Create a hasher that will check whether the password is MD5 and, if so, update it to PBKDF2.
- Handle MD5 Password Rehashing: When users log in with the MD5 hash, check the old hash, and if it matches, rehash it to PBKDF2.
- Save the new hash in the database: After rehashing, save the new PBKDF2 hash in the database.
Here’s how you can do this.
Step 1: Custom Password Hasher
from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import get_random_string
import hashlib
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = 'pbkdf2_wrapped_md5'
def verify(self, password, encoded):
"""
Verify that the password matches the encoded hash.
This will first check if the password is MD5, then
rehash it to PBKDF2 if the password matches.
"""
if encoded.startswith('md5$'):
# Extract the MD5 hash and salt from the encoded string
md5_hash = encoded[4:]
return self._verify_md5(password, md5_hash)
# If the password doesn't use MD5 hashing, fall back to standard verification.
return super().verify(password, encoded)
def _verify_md5(self, password, md5_hash):
"""
Verify the MD5 password hash.
"""
hashed = hashlib.md5(password.encode('utf-8')).hexdigest().upper()
if hashed == md5_hash:
# Rehash password with PBKDF2 and update it in the system
salt = get_random_string(length=8) # Generate a new salt
new_hash = self.encode(password, salt)
return new_hash
return False
def encode(self, password, salt):
"""
Override the encoding process to handle MD5 hashes.
"""
# First, we check if the password is an MD5 hash
if len(password) == 32: # MD5 is 32 chars
return 'md5$' + password.upper()
return super().encode(password, salt)
Step 2: Update Password Hashing in the Database
Now that you have a custom password hasher, the next step is to go through all existing user data and check for old MD5 passwords, then update them to PBKDF2.
from django.contrib.auth.models import User
from django.utils.crypto import get_random_string
# Assuming you have a model 'User' with passwords stored in MD5
for user in User.objects.all():
password = user.password # Old MD5 password hash
# Check if the password uses MD5 format
if password.startswith('md5$'):
hasher = PBKDF2WrappedMD5PasswordHasher()
# Extract the MD5 hash (after 'md5$')
md5_hash = password[4:]
# Rehash with PBKDF2
salt = get_random_string(length=8) # Random salt for PBKDF2
new_password = hasher.encode_md5_hash(md5_hash, salt)
# Update the user password with the new PBKDF2 hash
user.password = new_password
user.save()
Step 3: Update Your Django Settings
In your settings.py
, make sure you include the custom password hasher:
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
# Add your custom hasher here
{
'NAME': 'your_project.your_app.hashers.PBKDF2WrappedMD5PasswordHasher',
},
]
This will allow Django to use your custom password hasher to handle both MD5 and PBKDF2 hashes.
Step 4: Testing
Once the passwords have been rehashed to PBKDF2, you should be able to log in with the users' existing passwords (they will be hashed in MD5 format initially) and Django will update the password to PBKDF2 in the background. The user will continue to be authenticated normally.
Summary of the Key Steps:
- Custom hasher: The
PBKDF2WrappedMD5PasswordHasher
checks if the password is an MD5 hash. If so, it rehashes it with PBKDF2 and updates the password in the system. - Verification: The
verify()
method checks if the password is hashed with MD5. If so, it verifies it and updates it to PBKDF2. - Updating passwords: A script is used to iterate through all users, check if their password is MD5, and rehash it to PBKDF2 without requiring the user to log in again.
By doing this, you can securely migrate old MD5 passwords to PBKDF2, which is much more secure, while ensuring compatibility with your existing user data.