首页 > 解决方案 > Django升级未加盐的MD5密码不匹配

问题描述

我正在迁移一个使用未加盐 MD5 密码的旧系统(太可怕了!)。

我知道 Django会在用户登录时自动处理密码升级,方法是PASSWORD_HASHERSsettings.py.

但是,我想在不需要用户登录的情况下升级密码,文档中也有解释

所以,我按照文档中的示例并实现了一个自定义哈希,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 shellcheck_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_md5$150000$f3aae83b02e8727a2477644eb0aa6560$brqCWW5QuGUoSQ28YNPGUwTLEwZOuMNheN2RxVZGtHQ=
>>> check_password(test_pwd, upgraded_test_pwd)
False

我研究了其他 类似的 SO 问题,但也没有找到合适的解决方案。

标签: djangopasswords

解决方案


简短回答:通过不考虑提供的salt内容,在验证 Django 时(可能)无法提供相同的编码密码。

发生这种情况的原因是因为您凭空产生了“盐”,而忽略了salt通过的。事实上,如果我们看一下您的实现,我们会看到:

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)

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

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

我建议使用盐,例如:

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)

推荐阅读