Can't authenticate with custom PASSWORD_HASHERS

≡放荡痞女 提交于 2020-01-06 02:57:27

问题


I am working on the migration of one website with php to Django framework.

There is used to a specific hash passwords algorithm, so I had to write:

#settings.py
PASSWORD_HASHERS = (
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'project.hashers.SHA1ProjPasswordHasher',        # that's mine
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
 ...
)

and:

#hashers.py

import hashlib

from django.contrib.auth.hashers import (BasePasswordHasher, mask_hash)
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes
from django.utils.crypto import constant_time_compare
from django.utils.translation import ugettext_noop as _


class SHA1ProjPasswordHasher(BasePasswordHasher):
    """
    Special snowflake algorithm from the first version.
    php code: $pass=substr(sha1(trim($_POST['password'])),0,8);
    """
    algorithm = "unsalted_and_trimmed_sha1"

    def salt(self):
        return ''

    def encode(self, password, salt):
        return hashlib.sha1(force_bytes(salt + password)).hexdigest()[:8]

    def verify(self, password, encoded):
        encoded_2 = self.encode(password, '')
        return constant_time_compare(encoded, encoded_2)

    def safe_summary(self, encoded):
        return SortedDict([
            (_('algorithm'), self.algorithm),
            (_('hash'), mask_hash(encoded, show=3)),
            ])

It's works well when PBKDF2PasswordHasher is first:

>>> from django.contrib.auth import authenticate
>>> u = authenticate(username='root', password='test')
>>> u.password
u'pbkdf2_sha256$10000$EX8BcgPFjygx$HvB6NmZ7uX1rWOOPbHRKd8GLYD3cAsQtlprXUq1KGMk='
>>> exit()

Then I put my SHA1ProjPasswordHasher on the first place, first authentication works great. The hash was changed.:

>>> from django.contrib.auth import authenticate
>>> u = authenticate(username='root', password='test')
>>> u.password
'a94a8fe5'
>>> exit()

Second authentication is failed. Can't authenticate with new hash.

>>> from django.contrib.auth import authenticate
>>> u = authenticate(username='root', password='test')
>>> u.password
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'password'

What could be the problem? Thanks.


UPDATE: Ok, the problem became more clear. When I remove slice from here:

return hashlib.sha1(force_bytes(salt + password)).hexdigest()[:8]

everything working fine. I can't get why..


回答1:


Although years have passed, i' m putting my solution here for future reference

Vlad is partially right; The following method from django.contrib.auth.hashers seems to be forcing you to use a hash format that includes the dollar $ sign to mark the algorithm used for django to decide which hasher to use

def identify_hasher(encoded):
"""
Returns an instance of a loaded password hasher.
Identifies hasher algorithm by examining encoded hash, and calls
get_hasher() to return hasher. Raises ValueError if
algorithm cannot be identified, or if hasher is not loaded.
"""
# Ancient versions of Django created plain MD5 passwords and accepted
# MD5 passwords with an empty salt.
if ((len(encoded) == 32 and '$' not in encoded) or
(len(encoded) == 37 and encoded.startswith('md5$$'))):
algorithm = 'unsalted_md5'
# Ancient versions of Django accepted SHA1 passwords with an empty salt.
elif len(encoded) == 46 and encoded.startswith('sha1$$'):
algorithm = 'unsalted_sha1'
else:
algorithm = encoded.split('$', 1)[0]
return get_hasher(algorithm)

There is a way though to "trick" django without hacking your django installation. You 'll have to create an authentication backend to use for your authentication. There you'll override django's check_password method. I had a db where hashes were {SSHA512}hash and i couldn't change this because i had to be able to communicate with dovecot. So i put the following in my backends.py class:

def check_password(self, raw_password, user):
        """
        Returns a boolean of whether the raw_password was correct. Handles
        hashing formats behind the scenes.
        """
        def setter(raw_password):
            user.set_password(raw_password)
            user.save(update_fields=["password"])
        return check_password(raw_password, "SSHA512$" + user.password, setter)

That way when django has to check if a password is correct it'll do the following: -Get the hash from the db {SSHA512}hash -Append it temporarily a SSHA512$ string on the beginning and then check

So while you'll be having {SSHA512}hash in your database, when django is using this backend it'll see SSHA512${SSHA512}hash.

This way in your hashers.py you can set in your class algorithm = "SSHA512" which will hint django to use this hasher for that case.

Your def encode(self, password, salt, iterations=None) method in your hashers.py will save hashes the way dovecot needs {SSHA512}hash (you don't have to do anything weird in your encode method).

Your def verify(self, password, encoded) method though will have to strip the SSHA512$ "trick" from the encoded string that it is passed to compare it with the one that encode will create.

So there you have it! Django will use your hasher for checking hashes that don't contain a dollar $ sign and you don't have to break anything inside django :)




回答2:


Only unsalted md5 hashes can not include a dollar sign:

# django/contrib/auth/hashers.py

def identify_hasher(encoded):
    """
    Returns an instance of a loaded password hasher.

    Identifies hasher algorithm by examining encoded hash, and calls
    get_hasher() to return hasher. Raises ValueError if
    algorithm cannot be identified, or if hasher is not loaded.
    """
    if len(encoded) == 32 and '$' not in encoded:
        algorithm = 'unsalted_md5'
    else:
        algorithm = encoded.split('$', 1)[0]
    return get_hasher(algorithm)

So the best way is convert the current password hashes to the format: alg$salt$hash

class SHA1ProjPasswordHasher(BasePasswordHasher):
    """
    Special snowflake algorithm from the first version.
    php code: $pass=substr(sha1(trim($_POST['password'])),0,8);
    """
    algorithm = "unsalted_and_trimmed_sha1"

    def salt(self):
        return ''

    def encode(self, password, salt):
        assert password
        assert '$' not in salt
        hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()[:8]
        return "%s$%s$%s" % (self.algorithm, salt, hash)

    def verify(self, password, encoded):
        algorithm, salt, hash = encoded.split('$', 2)
        assert algorithm == self.algorithm
        encoded_2 = self.encode(password, salt)
        return constant_time_compare(encoded, encoded_2)

    def safe_summary(self, encoded):
        algorithm, salt, hash = encoded.split('$', 2)
        assert algorithm == self.algorithm
        return SortedDict([
            (_('algorithm'), algorithm),
            (_('salt'), mask_hash(salt, show=2)),
            (_('hash'), mask_hash(hash)),
            ])

.

>>> from django.contrib.auth import authenticate
>>> x = authenticate(username='root', password='test')
>>> x
<User: root>
>>> x.password
u'unsalted_and_trimmed_sha1$$a94a8fe5'


来源:https://stackoverflow.com/questions/14086413/cant-authenticate-with-custom-password-hashers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!