Django 升级未加盐的 MD5 密码不匹配

Django upgrading unsalted MD5 password not matching

我正在迁移一个使用未加盐 MD5 密码的旧系统(恐怖!)。

我知道 Django handles automatically password upgrading,当用户登录时,通过向 settings.py 中的 PASSWORD_HASHERS 列表添加额外的散列器。

但是,我想升级密码而不需要用户登录,also explained in the docs

所以,我按照文档中的示例实现了自定义哈希器,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)

并将其添加到 settings.py:

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "legacy.hashers.PBKDF2WrappedMD5PasswordHasher",
]

但是,在 Django shell check_password 中对此进行测试,升级后的密码返回 False。

>>> 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_md50000$f3aae83b02e8727a2477644eb0aa6560$brqCWW5QuGUoSQ28YNPGUwTLEwZOuMNheN2RxVZGtHQ=
>>> check_password(test_pwd, upgraded_test_pwd)
False

我已经研究了 other similar 个 SO 问题,但也没有找到合适的解决方案。

简答:通过不考虑提供的salt,在验证 Django 时不能(可能)得出相同的编码密码。

之所以会出现这种情况,是因为你凭空生成了"salt",而忽略了传入的salt。事实上,如果我们看一下您的实施,我们会看到:

class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
    algorithm = "pbkdf2_wrapped_md5"

    def encode_md5_hash(self, md5_hash):
        <b>salt</b> = secrets.token_hex(16)  # generating random salt
        return super().encode(md5_hash, salt)

    def encode(self, password, <b>salt</b>, iterations=None):
        md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt='')
        return self.encode_md5_hash(md5_hash)

传递给encode(..)方法的salt因此被忽略。

这意味着如果您稍后想要验证密码,Django 将使用它存储的salt调用encode(..)(在您的情况下,那是编码密码的第二部分,所以 f3aae83b02e8727a2477644eb0aa6560),但你决定扔掉它,并用不同的盐生成密码,因此编码密码不再与你存储的密码匹配在数据库中。

我建议使用盐,例如:

class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
    algorithm = "pbkdf2_wrapped_md5"

    def encode_md5_hash(self, md5_hash, <b>salt</b>):
        return super().encode(md5_hash, <b>salt</b>)

    def encode(self, password, <b>salt</b>, iterations=None):
        md5_hash = UnsaltedMD5PasswordHasher().encode(password, salt='')
        return self.encode_md5_hash(md5_hash<b>, salt</b>)