在执行任何进一步操作之前,请先了解加密和身份验证之间的区别,以及为什么您可能希望进行身份验证的加密而不仅仅是加密。
要实现经过身份验证的加密,您需要先加密,然后再加密 MAC。加密和身份验证的顺序非常重要!这个问题的现有答案之一犯了这个错误;就像许多用PHP编写的密码学库一样。
您应该避免实现自己的加密技术,而应使用由加密专家编写并由其审阅的安全库。
更新:PHP 7.2 现在提供 libsodium!为了获得最佳安全性,请更新您的系统以使用 PHP 7.2 或更高版本,并且仅遵循此答案中的 libsodium 建议。
如果您有PECL访问权限,请使用libsodium(或者如果您想要没有PECL的libsodium,sodium_compat 请使用libsodium);否则...
使用defuse/php-encryption;不要滚动你自己的加密!
上面链接的两个库都可以轻松轻松地将经过身份验证的加密实施到您自己的库中。
如果您仍然想编写和部署自己的加密库,则与Internet上每个密码学专家的传统智慧相反,这些是您必须采取的步骤。
加密:
- 在 CTR 模式下使用 AES 进行加密。您也可以使用GCM(这消除了对单独MAC的需求)。此外,ChaCha20 和 Salsa20(由 libsodium 提供)是流密码,不需要特殊模式。
- 除非您在上面选择了 GCM,否则您应该使用 HMAC-SHA-256(或者,对于流密码,Poly1305 -- 大多数 libsodium API 都会为您执行此操作)对密文进行身份验证。MAC应该涵盖IV以及密文!
解密:
- 除非使用 Poly1305 或 GCM,否则请重新计算密文的 MAC,并将其与使用
hash_equals()
发送的 MAC 进行比较。如果失败,请中止。
- 解密消息。
其他设计注意事项:
- 永远不要压缩任何东西。密文不可压缩;在加密之前压缩明文可能会导致信息泄露(例如,TLS上的犯罪和违规)。
- 确保使用 和 ,使用字符集模式以防止出现问题。
mb_strlen()
mb_substr()
'8bit'
mbstring.func_overload
- IV应该使用CSPRNG生成;如果您正在使用 ,请勿使用
MCRYPT_RAND
!mcrypt_create_iv()
- 除非您使用的是AEAD构造,否则请始终加密然后MAC!
-
bin2hex()
等可能会通过缓存计时泄露有关加密密钥的信息。如果可能的话,避免使用它们。base64_encode()
即使您遵循此处给出的建议,密码学也可能会出现很多问题。始终让加密专家审查您的实现。如果您没有幸运地与当地大学的密码学学生成为私人朋友,您可以随时尝试Cryptography Stack Exchange论坛以获取建议。
如果您需要对您的实现进行专业分析,您可以随时聘请信誉良好的安全顾问团队来审查您的PHP加密代码(披露:我的雇主)。
重要提示:何时不使用加密
不要加密密码。您希望改用以下密码哈希算法之一对它们进行哈希处理:
切勿使用通用哈希函数 (MD5、SHA256) 进行密码存储。
不要加密 URL 参数。这是工作的错误工具。
使用 Libsodium 的 PHP 字符串加密示例
如果您使用的是 PHP < 7.2 或未安装 libsodium,则可以使用 sodium_compat 来完成相同的结果(尽管速度较慢)。
<?php
declare(strict_types=1);
/**
* Encrypt a message
*
* @param string $message - message to encrypt
* @param string $key - encryption key
* @return string
* @throws RangeException
*/
function safeEncrypt(string $message, string $key): string
{
if (mb_strlen($key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new RangeException('Key is not the correct size (must be 32 bytes).');
}
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = base64_encode(
$nonce.
sodium_crypto_secretbox(
$message,
$nonce,
$key
)
);
sodium_memzero($message);
sodium_memzero($key);
return $cipher;
}
/**
* Decrypt a message
*
* @param string $encrypted - message encrypted with safeEncrypt()
* @param string $key - encryption key
* @return string
* @throws Exception
*/
function safeDecrypt(string $encrypted, string $key): string
{
$decoded = base64_decode($encrypted);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
if (!is_string($plain)) {
throw new Exception('Invalid MAC');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return $plain;
}
然后进行测试:
<?php
// This refers to the previous code block.
require "safeCrypto.php";
// Do this once then store it somehow:
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$message = 'We are all living in a yellow submarine';
$ciphertext = safeEncrypt($message, $key);
$plaintext = safeDecrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
盐石 - 利博钠变得更容易
我一直在做的一个项目是一个名为Halite的加密库,它旨在使libsodium更容易,更直观。
<?php
use \ParagonIE\Halite\KeyFactory;
use \ParagonIE\Halite\Symmetric\Crypto as SymmetricCrypto;
// Generate a new random symmetric-key encryption key. You're going to want to store this:
$key = new KeyFactory::generateEncryptionKey();
// To save your encryption key:
KeyFactory::save($key, '/path/to/secret.key');
// To load it again:
$loadedkey = KeyFactory::loadEncryptionKey('/path/to/secret.key');
$message = 'We are all living in a yellow submarine';
$ciphertext = SymmetricCrypto::encrypt($message, $key);
$plaintext = SymmetricCrypto::decrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
所有底层加密都由libsodium处理。
使用 defuse/php-encryption 的示例
<?php
/**
* This requires https://github.com/defuse/php-encryption
* php composer.phar require defuse/php-encryption
*/
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
require "vendor/autoload.php";
// Do this once then store it somehow:
$key = Key::createNewRandomKey();
$message = 'We are all living in a yellow submarine';
$ciphertext = Crypto::encrypt($message, $key);
$plaintext = Crypto::decrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
注意:返回十六进制编码的输出。Crypto::encrypt()
加密密钥管理
如果您想使用“密码”,请立即停止。您需要一个随机的128位加密密钥,而不是人类令人难忘的密码。
您可以存储加密密钥以供长期使用,如下所示:
$storeMe = bin2hex($key);
而且,根据要求,您可以像这样检索它:
$key = hex2bin($storeMe);
我强烈建议只存储一个随机生成的密钥以供长期使用,而不是任何类型的密码作为密钥(或派生密钥)。
如果您使用的是Defeuse的库:
“但我真的很想使用密码。
这是一个坏主意,但好吧,这是如何安全地做到这一点。
首先,生成一个随机密钥并将其存储在常量中。
/**
* Replace this with your own salt!
* Use bin2hex() then add \x before every 2 hex characters, like so:
*/
define('MY_PBKDF2_SALT', "\x2d\xb7\x68\x1a\x28\x15\xbe\x06\x33\xa0\x7e\x0e\x8f\x79\xd5\xdf");
请注意,您正在添加额外的工作,并且可以将此常量用作关键,并节省很多心痛!
然后使用PBKDF2(像这样)从密码中获取合适的加密密钥,而不是直接使用密码加密。
/**
* Get an AES key from a static password and a secret salt
*
* @param string $password Your weak password here
* @param int $keysize Number of bytes in encryption key
*/
function getKeyFromPassword($password, $keysize = 16)
{
return hash_pbkdf2(
'sha256',
$password,
MY_PBKDF2_SALT,
100000, // Number of iterations
$keysize,
true
);
}
不要只使用 16 个字符的密码。您的加密密钥将被严重破坏。