PHP的crypt()生成的MD5加密密码在Django密码字段中可用吗?

8 投票
3 回答
3172 浏览
提问于 2025-04-17 01:43

我正在把一些用户账号从一个老旧的PHP网站迁移到一个新的基于Django的网站。很多密码是用PHP的crypt()函数生成的MD5哈希值(可以参考那里的第三个例子)。

假设我们有这样一个来自老旧应用的密码哈希:

$1$f1KtBi.v$nWwBN8CP3igfC3Emo0OB8/

我该如何把它转换成Django所需的md5$<salt>$<hash>格式呢?看起来,crypt()生成的MD5输出使用的字符集和Django的MD5支持(似乎是用十六进制表示)不太一样。

更新:

有一个类似的问题,虽然没有人回答,但里面有一个有趣的潜在解决方案,可以把PHP的哈希转换成十六进制编码。不过根据我初步的尝试,似乎并不能生成可用的MD5十六进制值。 :(

具体例子:

给出一个具体的例子可能会更清楚。

假设:

  • 密码是foo
  • 盐值是$1$aofigrjlh

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

这里的crypt()是在MD5模式下运行,但它使用的是一种奇怪的丹麦翻译的MD5算法(更新:它是MD5-Crypt)。由于Python是一种源自荷兰的语言,Python的crypt模块只支持DES风格的哈希。

在Python中,我需要能够根据原始密码和盐值,重现这个哈希值或其某种常规衍生形式。

3 个回答

1

我正在把我的网站从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',
)

hashers.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)),
        ])
2

看看这个链接:passlib.hash.md5_crypt,这是一个很棒的项目叫做passlib

7

很遗憾,不能直接把这些转换成Django的格式(不过有一种方法可以让你的哈希值导入,下面会详细说明)。

Django使用的盐值md5算法其实很简单:md5(盐值 + 密码),然后把结果编码成十六进制。

而PHP的crypt()函数输出的哈希值,如果以$1$开头,那就不是简单的md5哈希了。它使用的是一种叫做MD5-Crypt的密码哈希算法。这种算法比简单的md5哈希复杂得多(也更安全)。链接页面中有一部分专门讲解MD5-Crypt的格式和算法。由于Django的代码不支持这种算法,所以无法将其转换为Django的格式。

虽然Django确实有调用Python标准库的crypt()函数的代码,但Django处理哈希的方式使得无法简单地将以$1$开头的哈希值通过Django传递到crypt()中;而这正是告诉crypt()你想使用MD5-Crypt而不是旧的DES-Crypt的唯一方法。


不过,有一种可能的解决方案:你可以对django.contrib.auth.models.User进行修改,使其同时支持普通的Django哈希和MD5-Crypt格式。这样你就可以不改变哈希值直接导入。可以通过手动重写User.set_passwordUser.check_password方法来实现。

另一种选择是使用Passlib库,它包含一个为Django设计的应用,可以处理这些问题,并提供md5-crypt等的跨平台支持。(免责声明:我是这个库的作者)不过这个Django插件没有文档,因为我没有在我的Django部署之外进行过多测试……不过在我的部署中运行得很好 :) (在源代码中有一些测试文档) 编辑:从Passlib 1.6开始,这个扩展现在已经正式发布并且有文档了。

要使用它,首先安装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的支持,这样你的哈希值就可以直接被接受了。

撰写回答