nunos
nunos

Reputation: 21429

Django upgrading unsalted MD5 password not matching

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

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

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

Related Questions