来自 PHP 的 crypt() 的 MD5 哈希密码是不是可移植到 Django 密码字段?

Posted

技术标签:

【中文标题】来自 PHP 的 crypt() 的 MD5 哈希密码是不是可移植到 Django 密码字段?【英文标题】:Are MD5-hashed passwords from PHP's crypt() portable to the Django password field?来自 PHP 的 crypt() 的 MD5 哈希密码是否可移植到 Django 密码字段? 【发布时间】:2011-11-12 09:49:20 【问题描述】:

我正在将一堆用户帐户从一个旧的 php 网站移植到一个新的、闪亮的基于 Django 的网站。一堆密码被存储为 PHP 的 crypt() 函数的 MD5 哈希输出(参见那里的第三个示例)。

鉴于来自旧版应用程序的密码哈希:

$1$f1KtBi.v$nWwBN8CP3igfC3Emo0OB8/

如何将其转换为 md5$<salt>$<hash> 的 Django 形式? crypt() MD5 输出似乎使用了与 Django 的 MD5 支持不同的字母表(似乎使用了十六进制摘要)。

更新:

similar (and unanswered) question 有一个有趣的潜在解决方案,可以将 PHP 哈希转换为 base-16 编码,但基于一些初始戳,它似乎无法生成可用的 MD5 十六进制摘要。 :(

具体例子:

一个具体的例子可能会有所帮助。

给定:

密码foo $1$aofigrjlh 的盐

在 PHP 中,crypt('foo', '$1$aofigrjlh') 生成 $1$aofigrjl$xLnO.D8x064D1kDUKWwbX. 的哈希值。

crypt() 在 MD5 模式下运行,但它是 MD5 算法的一些 wacky Danish translation(更新: 它是 MD5-Crypt)。由于 Python 是 Dutch-derived language,因此 Python 的 crypt 模块仅支持 DES 样式的散列。

在 Python 中,我需要能够在给定原始密码和盐的情况下重现该哈希值,或者它的一些常规派生。

【问题讨论】:

Django 说它支持crypt()。也许试一试,看看您是否可以破坏盐和编码以使它们匹配。但我会警惕任何像这样强制执行你所做的事情的框架(这样你就不能使用需求驱动的方法)...... Python 的 crypt 在 DES 模式下等效于 PHP 的 crypt(),而不是 MD5。 您是否尝试过将其置于md5$$1$f1KtBi.v$$nWwBN8CP3igfC3Emo0OB8/ 的形式中并针对已知密码进行测试?还是说salt和hash在Django格式中是十六进制的? Python 的 crypt 等同于 PHP 的 crypt() 。 Django 去掉了需要的$1$,这让这很不安。所以 Python 支持多种模式的 crypt,但 Django 不支持(恕我直言,这是一个大问题)。 @waitinfo:由于 Django 的操作方式,这将不起作用:check_password。它在$ 上分裂,因此导致其他加密方法无法使用... 【参考方案1】:

不幸的是,无法将它们转换为 Django 的格式(尽管您可以采取一种可能的方法来导入您的哈希值,详情如下)。

Django 的 salted md5 算法使用一个非常简单的算法:md5(salt + password),然后将其编码为十六进制。

另一方面,PHP 的crypt() 输出的以$1$ 开头的哈希不是简单的md5 哈希。相反,他们使用称为MD5-Crypt 的密码散列算法。这比简单的 md5 哈希要复杂得多(也更安全)。链接页面中有一个部分描述了 MD5-Crypt 格式和算法。无法将其转换为 Django 的格式,因为它不支持其代码中的算法。

虽然 Django 确实 有调用 Python 的 stdlib crypt() 函数的代码,但 Django 破坏哈希的方式意味着没有简单的方法可以通过 Django 和进入crypt();这是向crypt() 表明您想使用 MD5-Crypt 而不是旧的 DES-Crypt 的唯一方法。


但是,有一条可能的路线:您可以使用monkeypatch django.contrib.auth.models.User 使其同时支持普通的Django 哈希以及MD5-Crypt 格式。这样您就可以原封不动地导入散列。一种方法是手动执行此操作,方法是覆盖 User.set_passwordUser.check_password 方法。

另一种选择是使用 Passlib 库,其中包含一个 Django 应用程序,旨在处理所有这些问题,并为 md5-crypt 等提供跨平台支持。 (免责声明:我是该库的作者)不幸的是,Django 插件没有记录,因为我没有在我自己的 django 部署之外对其进行过多测试......虽然它对他们来说很好:)(source 中有一些 beta 文档) edit:从 Passlib 1.6 开始,此扩展现已正式发布,documented。

要使用它,请安装 passlib,并将 passlib.ext.django 添加到已安装应用程序列表中。然后,在settings.py 中,添加以下内容:

PASSLIB_CONFIG = """
[passlib]
schemes =
    md5_crypt,
    django_salted_sha1, django_salted_md5,
    django_des_crypt, hex_md5,
    django_disabled

default = md5_crypt

deprecated = django_des_crypt, hex_md5
"""

这将覆盖 User.set_passwordUser.check_password 以使用 Passlib 而不是内置代码。上面的配置字符串将 passlib 配置为模仿 Django 的内置哈希,但随后添加了对 md5_crypt 的支持,因此您的哈希应该被原样接受。

【讨论】:

一个小问题:你建议的PASSLIB_CONTEXT 使md5_crypt 成为默认值,这不一定是我想要做的。我只想添加对 scheem 的支持,而不是切换到它。 啊。在这种情况下,您可以将其更改为 default = django_salted_sha1。如果需要,您还可以将 md5_crypt 添加到已弃用的哈希列表中,用户将在登录时将其哈希迁移到默认值。(另外,出于安全考虑,我建议将 sha512_crypt 添加到列表中方案,并且默认情况下,因为它比 md5-crypt 或任何 django 的哈希更安全......它只是 django 的非标准)。该字符串中的选项对应于构造函数选项,详细信息为here。 非常好。我喜欢将密码迁移到更强的哈希值的想法,因为我还有一些使用 DES-Crypt 的旧帐户。【参考方案2】:

查看passlib.hash.md5_crypt,由很棒的passlib 项目提供。

【讨论】:

【参考方案3】:

我正在从 Wordpress 2.8 迁移到 Django 1.8。我发现 Wordpress 2.8(可能还有未来的版本)以 MD5 加密格式(phpass 库)存储密码。我尝试了 Django 1.8 的 passlib 扩展,但它对我不起作用。所以我最终用 MD5 加密算法编写了自定义哈希。

注意:在迁移期间将“md5_crypt”添加到密码哈希(user_pass 字段)

我将 MD5CryptPasswordHasher 添加到列表顶部以使其成为默认值(为了不混淆不同的哈希算法,如果我将再次迁移到另一个平台怎么办?)但它可以添加到列表底部如果只想为现有用户添加对算法的支持但强制新用户迁移到 PBKDF2PasswordHasher 哈希或其他。

settings.py

PASSWORD_HASHERS = (
    'your_project_name.hashers.MD5CryptPasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
    'django.contrib.auth.hashers.SHA1PasswordHasher',
    'django.contrib.auth.hashers.MD5PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
    'django.contrib.auth.hashers.CryptPasswordHasher',
)

hasers.py

import math
import hashlib
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import get_random_string
from django.contrib.auth.hashers import mask_hash
from collections import OrderedDict
from django.utils.translation import ugettext, ugettext_lazy as _

itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
def encode64(inp, count):
    outp = ''
    cur = 0
    while cur < count:
        value = inp[cur]
        cur += 1
        outp += itoa64[value & 0x3f]
        if cur < count:
            value |= (inp[cur] << 8)
        outp += itoa64[(value >> 6) & 0x3f]
        if cur >= count:
            break
        cur += 1
        if cur < count:
            value |= (inp[cur] << 16)
        outp += itoa64[(value >> 12) & 0x3f]
        if cur >= count:
            break
        cur += 1
        outp += itoa64[(value >> 18) & 0x3f]
    return outp.encode()

def crypt_private(pw, algorithm, code, salt, iterations):
    header = "%s$%s$%s%s" % (algorithm, code, itoa64[int(math.log(iterations, 2))], salt)
    pw = pw.encode()
    salt = salt.encode()
    hx = hashlib.md5(salt + pw).digest()
    while iterations:
        hx = hashlib.md5(hx + pw).digest()
        iterations -= 1
    return header + encode64(hx, 16).decode()


def get_md5_crypto_hash_params(encoded):
    algorithm, code, rest = encoded.split('$', 2)
    count_log2 = itoa64.find(rest[0])
    iterations = 1 << count_log2
    salt = rest[1:9]
    return (algorithm, salt, iterations)

class MD5CryptPasswordHasher(BasePasswordHasher):
    """
    The Salted MD5 Crypt password hashing algorithm that is used by Wordpress 2.8
    WARNING!
    The algorithm is not robust enough to handle any kind of MD5 crypt variations
    It was stripped and refactored based on passlib implementations especially for Wordpress 2.8 format
    """
    algorithm = "md5_crypt"

    iterations = 8192
    code = "P" # Modular Crypt prefix for phpass
    salt_len = 8

    def salt(self):
        return get_random_string(salt_len)

    def encode(self, password, salt):
        assert password is not None
        assert salt != ''
        return crypt_private(password, self.algorithm, self.code, salt, self.iterations)
        pass

    def verify(self, password, encoded):
        algorithm, salt, iterations = get_md5_crypto_hash_params(encoded)
        assert algorithm == self.algorithm
        return crypt_private(password, algorithm, self.code, salt, iterations) == encoded


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

【讨论】:

以上是关于来自 PHP 的 crypt() 的 MD5 哈希密码是不是可移植到 Django 密码字段?的主要内容,如果未能解决你的问题,请参考以下文章

制作哈希的 md5 哈希

介绍几个PHP 自带的加密解密函数

php 自带加密解密函数

如何在 NODE.JS 上模拟 php crypt()

PHP密码和token

PHP-密码和token