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

时间:2011-09-07 18:17:32

标签: php python django passwords md5

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

从遗留应用程序中获取此密码哈希:

$1$f1KtBi.v$nWwBN8CP3igfC3Emo0OB8/

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

更新

有一个similar (and unanswered) question有一个有趣的潜在解决方案,可以将PHP哈希转换为base-16编码,但基于一些初步的戳,它似乎不会产生可用的MD5 hexdigest。 :(

具体示例:

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

假设:

  • 密码为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中,我需要能够在给定原始密码和盐的情况下重现该哈希或其定期派生。

3 个答案:

答案 0 :(得分:7)

不幸的是,将它们转换为Django的格式是不可能的(尽管你可以采取一种可能的方法来获取你的哈希值,详见下文)。

Django的盐渍md5算法使用了一种非常简单的算法:md5(salt + password),然后编码为十六进制。

另一方面,以crypt()开头的PHP $1$输出的哈希值不是简单的md5哈希值。相反,他们使用称为MD5-Crypt的密码哈希算法。这比简单的md5哈希要复杂得多(而且安全)。链接页面中有一节描述了MD5-Crypt格式&amp;算法。没有办法将其转换为Django的格式,因为它没有在其代码中提供对算法的支持。

虽然Django 拥有调用Python的stdlib crypt()函数的代码,但是Django破坏哈希的方式意味着没有简单的方法来获取以$1$开头的哈希所有通过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文档)< / strike> 编辑:自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的支持,因此您的哈希应该按原样接受。

答案 1 :(得分:2)

通过精彩的passlib.hash.md5_crypt项目查看passlib

答案 2 :(得分:1)

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

注意:在迁移过程中添加&#34; md5_crypt&#34;密码哈希(user_pass字段)

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

<强> 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)),
        ])