Reputation: 21429
I am migrating an old system that uses unsalted MD5 passwords (the horror!).
I know Django handles automatically password upgrading, as users log in, by adding additional hashers to the PASSWORD_HASHERS
list in settings.py
.
But, I would like to upgrade the passwords without requiring users to log in, also explained in the docs.
So, I've followed the example in the docs and implemented a custom hasher, legacy/hasher.py
:
import secrets
from django.contrib.auth.hashers import PBKDF2PasswordHasher, UnsaltedMD5PasswordHasher
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash):
salt = secrets.token_hex(16)
return super().encode(md5_hash, salt)
def encode(self, password, salt, iterations=None):
md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt="")
return self.encode_md5_hash(md5_hash)
and add it to settings.py
:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"legacy.hashers.PBKDF2WrappedMD5PasswordHasher",
]
However, testing this in the Django shell check_password
is returning False for the upgraded password.
>>> from django.contrib.auth.hashers import check_password, UnsaltedMD5PasswordHasher
>>> from legacy.hashers import PBKDF2WrappedMD5PasswordHasher
>>> hasher = PBKDF2WrappedMD5PasswordHasher()
>>> test_pwd = '123456'
>>> test_pwd_unsalted_md5 = UnsaltedMD5PasswordHasher().encode(test_pwd, salt='')
>>> print(test_pwd_unsalted_md5)
'827ccb0eea8a706c4c34a16891f84e7b' # this is an example of a password I want to upgrade
>>> upgraded_test_pwd = hasher.encode_md5_hash(test_pwd)
>>> print(upgraded_test_pwd)
pbkdf2_wrapped_md5$150000$f3aae83b02e8727a2477644eb0aa6560$brqCWW5QuGUoSQ28YNPGUwTLEwZOuMNheN2RxVZGtHQ=
>>> check_password(test_pwd, upgraded_test_pwd)
False
I've looked into other similar SO questions, but didn't found a proper solution there as well.
Upvotes: 1
Views: 258
Reputation: 477881
Short answer: by not taking the provided salt
into account, when verifying Django can not (likely) come up with the same encoded password.
The reason this happens is because you generate "salt" out of thin air, and ignore the salt
that is passed. Indeed, if we take a look at your implementation, we see:
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash):
salt = secrets.token_hex(16) # generating random salt
return super().encode(md5_hash, salt)
def encode(self, password, salt, iterations=None):
md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt='')
return self.encode_md5_hash(md5_hash)
The salt
that is passed to the encode(..)
method is thus ignored.
This means that if you later want to verify the password, Django will call encode(..)
with the salt
that it stored (in your case, that is the second part of the encoded password, so f3aae83b02e8727a2477644eb0aa6560
), but you decide to throw that away, and generate the password with different salt, and therefore the encoded password, does no longer match with the password you did store in the database.
I advice to use the salt, for example with:
class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_wrapped_md5"
def encode_md5_hash(self, md5_hash, salt):
return super().encode(md5_hash, salt)
def encode(self, password, salt, iterations=None):
md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt='')
return self.encode_md5_hash(md5_hash, salt)
Upvotes: 3