Custom password hashing

Hi everyone - I’m working on a project to update a legacy site and migrate it to a new platform. It was using PHP, and I’m working on switching over to Django.

As part of the project, I want to update the password hashing. The legacy site had two password hashers - a bcrypt hasher built-in to PHP and an older custom-built hmac hasher.

The bcrypt one isn’t too hard, but I also need to be able to support the older hmac hashing. I just need to use the custom hasher to verify when a password is correct and then I’ll let Django update it to the PBKDF2.

My questions are about how to implement the custom password hasher. Let’s call the new password hasher LegacyPasswordHasher. It looks like I need to implement the following (leaving out the implementation details of the hashing algorithm since it’s not really relevant to the discussion):

class LegacyPasswordHasher(BasePasswordHasher):
    algorithm = 'legacy_hasher'

    def encode(self, password: str, salt: str) -> Any:
        # Implementation details go here

    def verify(self, password: str, encoded: str) -> bool:
        # Implementation details go here

    def safe_summary(self, encoded: str) -> Any:
        # Implementation details go here

And then I need to add this to settings.py:

# Password hashing
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.BCryptPasswordHasher',
    'mysite.hashers.LegacyPasswordHasher'
]

And now, my questions:

How does Django select which password hasher to use? Does it just try all of the password hashers in the list and fail if verify() returns false for all of them? Or does it have some criterion it uses for selecting a specific hasher from the list?

When verifying a login attempt, what is Django’s order of operations for a hasher? For example, does it just call verify, or does it call safe_summary and then verify, or something else?

Do I need to implement all three functions in my custom hasher if I’m only planning on verifying? I don’t want to save passwords using the legacy hasher, I just want to be able to log users in and then hash their password using PBKDF2.

Do I need to add any data to the hash stored in the database to indicate that it’s hashed using the legacy algorithm? An example hash may look like this: ce7a3ced60ceb06e746665fd5d22a2a0dfa187ec - should this be stored in the auth_user.password, or does it need some additional decorations to help Django identify which hasher to use? According to this documentation, a hash is normally stored in this format: <algorithm>$<iterations>$<salt>$<hash> - but I don’t know if this is required or just a convention.

Thanks in advance, I can provide further details if needed.

Yes, see the authenticate method in django.contrib.auth.__init__

Yes. The safe_summary method is not involved in the login process at all. See safe_summary in django.contrib.auth.hashers

You do need to implement all three, although they don’t need to be fully functional.
(See the implementation of ‘BasePasswordHasher’ in django.contrib.auth.hashers)

No. See the UnsaltedMD5PasswordHasher class in django.contrib.auth.hashers to see a complete implementation of a fairly minimal hasher.

That’s not the right section of code Ken. That’s the list of auth backends. The password hashers are independent.

The correct piece of code is here: django/hashers.py at 41329b9852fcae586bd20aa9f3e591dde94cc925 · django/django · GitHub

You can see that Django identifies which hasher to load based on the name encoded in the password hash string.

This wasn’t really documented well, so I’ve opened a PR to improve the relevant docs: Documented how contrib.auth picks a password hasher for verification. by adamchainz · Pull Request #15194 · django/django · GitHub . Feel free to review!

Btw rather than simply loading the old hashes as they are, you may wish to upgrade the password hashes. Django has a guide on this: Password management in Django | Django documentation | Django

1 Like

I believe the answer here is yes. The Unsalted* hashers inside Django are special cased: django/hashers.py at 41329b9852fcae586bd20aa9f3e591dde94cc925 · django/django · GitHub

1 Like

Thanks, I’ll look into this after I get the custom hasher working. Your responses have been helpful.