使用PHP最简单的双向加密


230

在普通的PHP安装中进行双向加密的最简单方法是什么?

我需要能够使用字符串密钥加密数据,并在另一端使用相同的密钥进行解密。

安全性不像代码的可移植性那么重要,因此我希望能够使事情尽可能简单。当前,我正在使用RC4实现,但是如果可以找到本机支持的东西,那么我可以节省很多不必要的代码。



3
对于通用加密,请使用defuse / php-encryption /而不是自己滚动。
Scott Arciszewski,2015年

2
远离github.com/defuse/php-encryption-它比mcrypt慢几个数量级。
Eugen Rieck

1
@Scott带着“这可能不会成为瓶颈”的思路思考,这是给我们带来许多不良软件的原因。
Eugen Rieck

3
如果您确实要加密/解密大量数据,以至于花费数毫秒的时间使您的应用程序陷入瘫痪,请硬着头皮切换到libsodium。Sodium::crypto_secretbox()而且Sodium::crypto_secretbox_open()安全又高效。
Scott Arciszewski 2015年

Answers:


196

编辑:

您确实应该使用openssl_encrypt()openssl_decrypt()

正如Scott所说,Mcrypt并不是一个好主意,因为自2007年以来就没有进行过更新。

甚至还有RFC可以从PHP中删除Mcrypt- https: //wiki.php.net/rfc/mcrypt-viking-funeral


6
@EugenRieck是的,这就是重点。Mcrypt没有收到补丁。一旦发现任何漏洞,无论大小,OpenSSL都会收到补丁。
格雷格

5
最好在回答时也提供最简单的示例,这样投票的答案会更好。不管怎么说,还是要谢谢你。
T.Todua '18年

伙计们,仅供参考=> MCRYPT已弃用。封顶,因此每个人都应该知道不要使用它,因为它给我们带来了许多问题。自从PHP 7.1起,如果我没有记错的话,它就弃用了。
clusterBuddy

从PHP 7开始,mcrypt函数已从php代码库中删除。因此,当使用最新版本的php(应该是标准版本)时,您将无法再使用此不推荐使用的功能。
亚历山大·贝林

234

重要提示:除非您有非常特殊的用例,否则不要加密密码,而应使用密码哈希算法。当有人说他们在服务器端应用程序中加密密码时,他们要么不了解情况,要么描述了危险的系统设计。安全存储密码是与加密完全不同的问题。

被告知。设计安全系统。

PHP中的可移植数据加密

如果您使用的是PHP 5.4或更高版本,并且不想自己编写加密模块,则建议您使用提供身份验证加密的现有库。我链接的库仅依赖于PHP提供的内容,并且受到少数安全研究人员的定期审查。(包括我自己。)

如果你的便携性的目标并不阻止要求PECL扩展,libsodium强烈建议在任何你或我可以用PHP编写。

更新(2016-06-12):现在,您可以使用sodium_compat并使用相同的crypto libsodium产品,而无需安装PECL扩展。

如果您想尝试密码学工程,请继续阅读。


首先,您应该花时间学习未经身份验证的加密加密厄运原理的危险

  • 恶意用户仍然可以篡改加密的数据。
  • 对加密数据进行身份验证可防止篡改。
  • 对未加密的数据进行身份验证不会阻止篡改。

加密与解密

加密在PHP中其实很简单(我们将使用openssl_encrypt()openssl_decrypt()一旦你作出了关于如何加密你的信息的一些决策咨询。openssl_get_cipher_methods()为您的系统支持的方法的列表最好的选择是,AES的CTR模式

  • aes-128-ctr
  • aes-192-ctr
  • aes-256-ctr

当前没有理由认为AES密钥大小是一个值得担心的重大问题(由于256位模式下的密钥调度不正确,因此更大的密钥可能并不更好)。

注意:我们不使用mcrypt它,因为它是废弃软件,并且存在未修补的漏洞,这些漏洞可能会影响安全性。由于这些原因,我鼓励其他PHP开发人员也避免使用它。

使用OpenSSL的简单加密/解密包装器

class UnsafeCrypto
{
    const METHOD = 'aes-256-ctr';

    /**
     * Encrypts (but does not authenticate) a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded 
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = openssl_random_pseudo_bytes($nonceSize);

        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );

        // Now let's pack the IV and the ciphertext together
        // Naively, we can just concatenate
        if ($encode) {
            return base64_encode($nonce.$ciphertext);
        }
        return $nonce.$ciphertext;
    }

    /**
     * Decrypts (but does not verify) a message
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = mb_substr($message, 0, $nonceSize, '8bit');
        $ciphertext = mb_substr($message, $nonceSize, null, '8bit');

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );

        return $plaintext;
    }
}

使用范例

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

演示https : //3v4l.org/jl7qR


上面的简单加密库仍然不安全使用。在解密之前,我们需要对密文进行身份验证并进行验证

注意:默认情况下,UnsafeCrypto::encrypt()将返回原始二进制字符串。如果您需要以二进制安全格式(以base64编码)存储它,请像这样调用它:

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key, true);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key, true);

var_dump($encrypted, $decrypted);

演示:http: //3v4l.org/f5K93

简单身份验证包装

class SaferCrypto extends UnsafeCrypto
{
    const HASH_ALGO = 'sha256';

    /**
     * Encrypts then MACs a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded string
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);

        // Pass to UnsafeCrypto::encrypt
        $ciphertext = parent::encrypt($message, $encKey);

        // Calculate a MAC of the IV and ciphertext
        $mac = hash_hmac(self::HASH_ALGO, $ciphertext, $authKey, true);

        if ($encode) {
            return base64_encode($mac.$ciphertext);
        }
        // Prepend MAC to the ciphertext and return to caller
        return $mac.$ciphertext;
    }

    /**
     * Decrypts a message (after verifying integrity)
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string (raw binary)
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        // Hash Size -- in case HASH_ALGO is changed
        $hs = mb_strlen(hash(self::HASH_ALGO, '', true), '8bit');
        $mac = mb_substr($message, 0, $hs, '8bit');

        $ciphertext = mb_substr($message, $hs, null, '8bit');

        $calculated = hash_hmac(
            self::HASH_ALGO,
            $ciphertext,
            $authKey,
            true
        );

        if (!self::hashEquals($mac, $calculated)) {
            throw new Exception('Encryption failure');
        }

        // Pass to UnsafeCrypto::decrypt
        $plaintext = parent::decrypt($ciphertext, $encKey);

        return $plaintext;
    }

    /**
     * Splits a key into two separate keys; one for encryption
     * and the other for authenticaiton
     * 
     * @param string $masterKey (raw binary)
     * @return array (two raw binary strings)
     */
    protected static function splitKeys($masterKey)
    {
        // You really want to implement HKDF here instead!
        return [
            hash_hmac(self::HASH_ALGO, 'ENCRYPTION', $masterKey, true),
            hash_hmac(self::HASH_ALGO, 'AUTHENTICATION', $masterKey, true)
        ];
    }

    /**
     * Compare two strings without leaking timing information
     * 
     * @param string $a
     * @param string $b
     * @ref https://paragonie.com/b/WS1DLx6BnpsdaVQW
     * @return boolean
     */
    protected static function hashEquals($a, $b)
    {
        if (function_exists('hash_equals')) {
            return hash_equals($a, $b);
        }
        $nonce = openssl_random_pseudo_bytes(32);
        return hash_hmac(self::HASH_ALGO, $a, $nonce) === hash_hmac(self::HASH_ALGO, $b, $nonce);
    }
}

使用范例

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = SaferCrypto::encrypt($message, $key);
$decrypted = SaferCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

演示原始二进制文件base64编码


如果有人希望SaferCrypto在生产环境或您自己的相同概念的实现中使用此库,我强烈建议在使用之前先与居民密码学家联系,以征询您的第二意见。他们将能够告诉您一些我什至不知道的错误。

使用信誉良好的加密库会使您更好。


3
因此,我只是想首先使UnsafeCrypto工作。加密进行得很好,但是每次运行解密时,都会得到“假”作为响应。我使用相同的密钥解密,并在编码和解码上传递true。我在示例中假设是打字机,我想知道这是否是我的问题所在。您能解释一下$ mac变量的来源吗,应该简单地将其为$ iv吗?
David C

1
@EugenRieck OpenSSL密码实现可能是唯一不被滥用的部分,并且是在原始PHP中利用AES-NI的唯一方法。如果您在OpenBSD上安装,则PHP将根据LibreSSL进行编译,而PHP代码不会有所不同。随时> Libsodium> OpenSSL。另外,不要使用libmcrypt您会推荐PHP开发人员使用什么代替OpenSSL?
Scott Arciszewski

2
不再支持5.2和5.3。相反,您应该考虑更新到受支持的PHP版本,例如5.6。
Scott Arciszewski 2015年


1
我只是为了说明您要使用二进制字符串而不是人类的readabale字符串作为密钥
Scott Arciszewski

22

使用mcrypt_encrypt()mcrypt_decrypt()以及相应的参数。确实非常简单明了,并且使用了经过考验的加密软件包。

编辑

在得到此答案的5年零4个月后,该mcrypt扩展程序已处于弃用状态,并最终从PHP中删除。


34
经过实战测试且超过8年未更新?
Maarten Bodewes,2014年

2
好吧,mcrypt在PHP7中并且没有被弃用-对我来说足够了。并非所有代码都具有OpenSSL的可怕质量,并且需要每隔几天修补一次。
Eugen Rieck

3
mcrypt不仅在支持方面令人恐惧。它还没有实现最佳实践,例如PKCS#7兼容的填充,经过身份验证的加密。它不支持SHA-3或任何其他新算法,因为没有人维护它,这使您失去了升级途径。此外,它曾经接受诸如部分键,执行零填充等操作。这是有理由将其逐步从PHP中删除的原因。
马滕·博德威斯

2
在PHP 7.1中,所有mcrypt_ *函数都将引发E_DEPRECATED通知。在PHP 7.1 + 1(无论是7.2还是8.0)中,mcrypt扩展将从核心移出,而在PECL中,如果希望从PECL安装PHP扩展,那么真正想要安装它的人仍然可以这样做。
Mladen Janjetovic

4

PHP 7.2完全脱离了Mcrypt这个领域,现在加密基于可维护的Libsodium库。

您的所有加密需求基本上都可以通过Libsodium库解决。

// On Alice's computer:
$msg = 'This comes from Alice.';
$signed_msg = sodium_crypto_sign($msg, $secret_sign_key);


// On Bob's computer:
$original_msg = sodium_crypto_sign_open($signed_msg, $alice_sign_publickey);
if ($original_msg === false) {
    throw new Exception('Invalid signature');
} else {
    echo $original_msg; // Displays "This comes from Alice."
}

Libsodium文档:https : //github.com/paragonie/pecl-libsodium-doc


2
如果粘贴一些代码,请确保覆盖所有变量。在您的示例中,$ secret_sign_key和$ alice_sign_publickey为NULL
undefinedman

1
crypto_signAPI确实加密的消息-这将需要的一个crypto_aead_*_encrypt功能。
罗杰·杜克

1

重要说明:此答案仅对PHP 5有效,在PHP 7中使用内置密码功能。

这是简单但足够安全的实现:

  • CBC模式下的AES-256加密
  • PBKDF2通过纯文本密码创建加密密钥
  • HMAC验证加密的消息。

代码和示例在这里:https : //stackoverflow.com/a/19445173/1387163


1
我不是密码专家,但是直接从密码派生密钥似乎是一个糟糕的主意。彩虹桌+弱密码而消失是您的安全。另外,您的链接指向mcrypt函数,该函数自PHP 7.1起已弃用
Alph.Dev

@ Alph.Dev您是正确的,以上答案仅对PHP 5有效
Eugene Fidelin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.