【问题标题】:Django password hasher using php format of function password_hash()Django密码哈希使用函数password_hash()的php格式
【发布时间】:2020-01-03 17:03:57
【问题描述】:

我必须添加一个向后兼容的 Django 应用程序,它支持保存在使用 PHP 函数 password_hash() 创建的数据库中的旧密码,其输出类似于

$2y$10$puZfZbp0UGMYeUiyZjdfB.4RN9frEMy8ENpih9.jOEngy1FJWUAHy

(10 轮散列的咸河豚加密算法)

Django 支持以算法名称为前缀的格式,所以如果我使用 BCryptPasswordHasher 作为主要的哈希输出将如下所示:

bcrypt$$2y$10$puZfZbp0UGMYeUiyZjdfB.4RN9frEMy8ENpih9.jOEngy1FJWUAHy

我已经创建了自定义 BCryptPasswordHasher,例如:

class BCryptPasswordHasher(BasePasswordHasher):
    algorithm = "bcrypt_php"
    library = ("bcrypt", "bcrypt")
    rounds = 10

    def salt(self):
        bcrypt = self._load_library()
        return bcrypt.gensalt(self.rounds)

    def encode(self, password, salt):
        bcrypt = self._load_library()
        password = password.encode()
        data = bcrypt.hashpw(password, salt)
        return f"{data.decode('ascii')}"

    def verify(self, incoming_password, encoded_db_password):
        algorithm, data = encoded_db_password.split('$', 1)
        assert algorithm == self.algorithm

        db_password_salt = data.encode('ascii')
        encoded_incoming_password = self.encode(incoming_password, db_password_salt)
        # Compare of `data` should only be done because in database we don't persist alg prefix like `bcrypt$`
        return constant_time_compare(data, encoded_incoming_password)

    def safe_summary(self, encoded):
        empty, algostr, work_factor, data = encoded.split('$', 3)
        salt, checksum = data[:22], data[22:]
        return OrderedDict([
            ('algorithm', self.algorithm),
            ('work factor', work_factor),
            ('salt', mask_hash(salt)),
            ('checksum', mask_hash(checksum)),
        ])

    def must_update(self, encoded):
        return False

    def harden_runtime(self, password, encoded):
        data = encoded.split('$')
        salt = data[:29]  # Length of the salt in bcrypt.
        rounds = data.split('$')[2]
        # work factor is logarithmic, adding one doubles the load.
        diff = 2 ** (self.rounds - int(rounds)) - 1
        while diff > 0:
            self.encode(password, salt.encode('ascii'))
            diff -= 1

和 AUTH_USER_MODEL 一样:

from django.contrib.auth.hashers import check_password
from django.db import models


class User(models.Model):
    id = models.BigAutoField(primary_key=True)
    email = models.EmailField(unique=True)
    password = models.CharField(max_length=120, blank=True, null=True)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []
    EMAIL_FIELD = 'email'

    def check_password(self, raw_password):
        def setter():
            pass

        alg_prefix = "bcrypt_php$"
        password_with_alg_prefix = alg_prefix + self.password
        return check_password(raw_password, password_with_alg_prefix, setter)

设置base.py:

...
AUTH_USER_MODEL = 'custom.User'

PASSWORD_HASHERS = [
    'custom.auth.hashers.BCryptPasswordHasher',
]
...

在这种情况下,在验证密码之前,我添加了bcrypt$前缀然后进行验证,但是在数据库中,密码保存在没有bcrypt$的情况下。

它有效,但我想知道是否有其他更简单的方法可以做到这一点,或者可能有人遇到同样的问题?

我想补充一点,PHP 应用程序和新的 Django 都应该支持这两种格式,我不能对旧的 PHP 进行更改。只能在新的 Django 服务器上进行更改。

【问题讨论】:

  • check_password() 函数内部的某处,您可以将计算的哈希值与有效值进行比较,您可以执行 calculated_hash[7:] == valid_hash_without_prefix
  • 您是说User.check_password() 还是django.contrib.auth.hasher.check_password()?第一个我可以修改但django.contrib.auth.hasher.check_password()我认为我不应该

标签: python django bcrypt django-authentication backwards-compatibility


【解决方案1】:

您可以使用自定义身份验证后端。 首先在您的身份验证应用程序中创建一个文件(假设您将其命名为 auth),调用文件 backends.py

backends.py 的内容

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend

UserModel = get_user_model()


class ModelBackend(BaseBackend):
    """
    Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD.

    Use the login name and a hash of the password. For example:

    ADMIN_LOGIN = 'admin'
    ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password):
                # user exists
                return user

然后在您的 settings.py 中将您的 backends.py 包含在 AUTHENTICATION_BACKENDS 变量中

它应该看起来像这样

AUTHENTICATION_BACKENDS = [
    "djangoprojectname.auth.backends.ModelBackend",
    "django.contrib.auth.backends.ModelBackend",
]

最后一点是在你的auth模型中实现check_password方法,在我们这里的例子中,我们在auth app中打开文件models.py,在类User中我们添加方法来实现基于bcrypt算法的密码检查。

import bcrypt

class User(AbstractUser):
    first_name = models.CharField(max_length=255, null=True)
    email = models.CharField(unique=True, max_length=255, blank=True, null=True)

    def check_password(self, raw_password):
        def setter():
            pass

        check = bcrypt.checkpw(bytes(raw_password, 'utf-8'), bytes(self.password, 'utf-8'))
        return check

    def set_password(self, raw_password):
        hashed = bcrypt.hashpw(bytes(raw_password, 'utf-8'), bcrypt.gensalt(rounds=10))
        encrypted = str(hashed, 'UTF-8')
        self.password = encrypted
        self._password = encrypted 

然后,在您的登录视图中,您可能必须在 auth 应用程序的 views.py 中实现类似的东西

username = data.get("username")
password = data.get("password")

if username is None or password is None:
    pass # implement for requesting username and password

user = authenticate(username=username, password=password)
if user is None:
    pass # implement for invalid credentials

# check user confirmation
confirmed = getattr(user, 'confirmed', None)
if confirmed is False or None:
    pass # implement for user not confirmed

# check if user is active
isactive = getattr(user, 'isactive', None)
if isactive is False or None:
    pass # implement for user account disabled

login(request, user)

# return view or json response or whatever for login success

【讨论】:

    【解决方案2】:

    解决密码检查使用 PHPpassword_hash(password, PASSWORD_DEFAULT)

    生成的哈希的逆向任务
    from django.contrib.auth.hashers import check_password
    check_password(decoded_pass, 'bcrypt${0}'.format(php_hash), preferred='bcrypt')
    

    为我工作。

    使用 Django 2.2 测试。 bcrypt$ 前缀破解基于 another SO answer

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-14
      • 2016-12-26
      • 1970-01-01
      • 2014-10-02
      • 2017-08-10
      • 1970-01-01
      • 1970-01-01
      • 2011-06-18
      相关资源
      最近更新 更多