像web2py一样在PHP中加密密码

2 投票
1 回答
980 浏览
提问于 2025-04-17 18:24

我想在PHP中实现web2py中的密码哈希功能。现在我得到了web2py的创建者Massimo De Piero的一些指导,但我还是无法在PHP中实现。指导内容如下:

这个逻辑比较复杂,因为它需要处理很多选项,并且要保持向后兼容。

通常,一个哈希后的密码看起来像这样:

算法$盐$哈希值
算法$$哈希值(没有盐)
哈希值(旧版)

哈希值是通过算法、盐值,以及可选的用户提供的密钥计算出来的。密钥是唯一的,而盐值对于每个密码都是不同的。

每次你调用CRYPT()('密码')时,你会得到一个LazyCrypt对象。这个对象可以被序列化成一个字符串。你得到的字符串总是不同,因为它包含一个随机的盐值。你不能比较这两个字符串,因为即使是同一个密码,你也总是会得到false。但你可以将一个LazyObject与一个字符串进行比较,这个懒对象会使用相同的算法和字符串中的盐值来计算哈希,并与字符串中的哈希进行比较。举个例子:

>>> a = CRYPT()('password')
>>> b = CRYPT()('password')
>>> sa = str(a)
>>> sb = str(b)
>>> sa == sb
False
>>> a == sb
True
>>> c = CRYPT()('wrong')
>>> c == sb
False

以下是web2py框架中用于实现这种加密的类:

class LazyCrypt(object):
    """
    Stores a lazy password hash
    """

    def __init__(self, crypt, password):
        """
        crypt is an instance of the CRYPT validator,
        password is the password as inserted by the user
        """
        self.crypt = crypt
        self.password = password
        self.crypted = None

    def __str__(self):
        """
        Encrypted self.password and caches it in self.crypted.
        If self.crypt.salt the output is in the format <algorithm>$<salt>$<hash>
        Try get the digest_alg from the key (if it exists)
        else assume the default digest_alg. If not key at all, set key=''
        If a salt is specified use it, if salt is True, set salt to uuid
        (this should all be backward compatible)
        Options:
        key = 'uuid'
        key = 'md5:uuid'
        key = 'sha512:uuid'
        ...
        key = 'pbkdf2(1000,64,sha512):uuid' 1000 iterations and 64 chars length
        """
        if self.crypted:
            return self.crypted
        if self.crypt.key:
            if ':' in self.crypt.key:
                digest_alg, key = self.crypt.key.split(':', 1)
            else:
                digest_alg, key = self.crypt.digest_alg, self.crypt.key
        else:
            digest_alg, key = self.crypt.digest_alg, ''
        if self.crypt.salt:
            if self.crypt.salt == True:
                salt = str(web2py_uuid()).replace('-', '')[-16:]
            else:
                salt = self.crypt.salt
        else:
            salt = ''

        hashed = simple_hash(self.password, key, salt, digest_alg)
        self.crypted = '%s$%s$%s' % (digest_alg, salt, hashed)
        return self.crypted

    def __eq__(self, stored_password):
        """
        compares the current lazy crypted password with a stored password
        """
        # LazyCrypt objects comparison
        if isinstance(stored_password, self.__class__):
            return ((self is stored_password) or
                   ((self.crypt.key == stored_password.crypt.key) and
                   (self.password == stored_password.password)))

        if self.crypt.key:
            if ':' in self.crypt.key:
                key = self.crypt.key.split(':')[1]
            else:
                key = self.crypt.key
        else:
            key = ''
        if stored_password is None:
            return False
        elif stored_password.count('$') == 2:
            (digest_alg, salt, hash) = stored_password.split('$')
            h = simple_hash(self.password, key, salt, digest_alg)
            temp_pass = '%s$%s$%s' % (digest_alg, salt, h)
        else:  # no salting
            # guess digest_alg
            digest_alg = DIGEST_ALG_BY_SIZE.get(len(stored_password), None)

            if not digest_alg:
                return False
            else:
                temp_pass = simple_hash(self.password, key, '', digest_alg)
        return temp_pass == stored_password


class CRYPT(object):

    """
    example::
        INPUT(_type='text', _name='name', requires=CRYPT())
    encodes the value on validation with a digest.
    If no arguments are provided CRYPT uses the MD5 algorithm.
    If the key argument is provided the HMAC+MD5 algorithm is used.
    If the digest_alg is specified this is used to replace the
    MD5 with, for example, SHA512. The digest_alg can be
    the name of a hashlib algorithm as a string or the algorithm itself.
    min_length is the minimal password length (default 4) - IS_STRONG for serious security
    error_message is the message if password is too short

    Notice that an empty password is accepted but invalid. It will not allow login back.

    Stores junk as hashed password.
    Specify an algorithm or by default we will use sha512.
    Typical available algorithms:
      md5, sha1, sha224, sha256, sha384, sha512

    If salt, it hashes a password with a salt.
    If salt is True, this method will automatically generate one.
    Either case it returns an encrypted password string in the following format:

      <algorithm>$<salt>$<hash>

    Important: hashed password is returned as a LazyCrypt object and computed only if needed.
    The LasyCrypt object also knows how to compare itself with an existing salted password

    Supports standard algorithms

    >>> for alg in ('md5','sha1','sha256','sha384','sha512'):

    ...     print str(CRYPT(digest_alg=alg,salt=True)('test')[0])
    md5$...$...
    sha1$...$...
    sha256$...$...
    sha384$...$...
    sha512$...$...

    The syntax is always alg$salt$hash

    Supports for pbkdf2
    >>> alg = 'pbkdf2(1000,20,sha512)'
    >>> print str(CRYPT(digest_alg=alg,salt=True)('test')[0])
    pbkdf2(1000,20,sha512)$...$...

    An optional hmac_key can be specified and it is used as salt prefix
    >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0])
    >>> print a
    md5$...$...

    Even if the algorithm changes the hash can still be validated
    >>> CRYPT(digest_alg='sha1',key='mykey',salt=True)('test')[0] == a
    True

    If no salt is specified CRYPT can guess the algorithms from length:
    >>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0])
    >>> a
    'sha1$$a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
    >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a
    True
    >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a[6:]
    True
    >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a
    True
    >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a[6:]
    True
    """

    def __init__(self,
                 key=None,
                 digest_alg='pbkdf2(1000,20,sha512)',
                 min_length=0,
                 error_message='too short', salt=True):

        """
        important, digest_alg='md5' is not the default hashing algorithm for
        web2py. This is only an example of usage of this function.

        The actual hash algorithm is determined from the key which is
        generated by web2py in tools.py. This defaults to hmac+sha512.
        """

        self.key = key
        self.digest_alg = digest_alg
        self.min_length = min_length
        self.error_message = error_message
        self.salt = salt

    def __call__(self, value):
        if len(value) < self.min_length:
            return ('', translate(self.error_message))
        return (LazyCrypt(self, value), None) 

希望有人能给我一点指导,
最好的祝愿,

1 个回答

0

你应该使用 password_hash()password_verify(),而不是直接使用 crypt()

撰写回答