在您进一步进行操作之前,请尝试了解加密和身份验证之间的区别,以及为什么您可能想要经过身份验证的加密而不是仅加密。
要实施经过身份验证的加密,您需要先加密然后再加密MAC。加密和身份验证的顺序非常重要!对这个问题的现有答案之一就是这个错误。就像许多用PHP编写的密码库一样。
您应该避免实施自己的加密技术,而应使用由加密专家编写和审查的安全库。
更新:PHP 7.2现在提供libsodium!为了获得最佳安全性,请更新系统以使用PHP 7.2或更高版本,并且仅遵循此答案中的libsodium建议。
如果您具有PECL访问权限,请使用libsodium(如果要没有PECL的libsodium,请使用sodium_compat);否则...
使用defuse / php-encryption ; 不要使用自己的密码!
上面链接的两个库都可以轻松轻松地将经过身份验证的加密实施到您自己的库中。
如果您仍然想编写和部署自己的密码库,那么与Internet上每位密码专家的传统智慧相反,这些都是您必须采取的步骤。
加密:
- 在CTR模式下使用AES加密。您也可以使用GCM(这样就不需要单独的MAC)。此外,ChaCha20和Salsa20(由libsodium提供)是流密码,不需要特殊模式。
- 除非您在上面选择了GCM,否则应使用HMAC-SHA-256(或对于流密码为Poly1305,大多数libsodium API会为您执行此操作)对密文进行身份验证。MAC应该包含IV和密文!
解密:
- 除非使用Poly1305或GCM,否则请重新计算密文的MAC,并将其与使用发送的MAC进行比较
hash_equals()
。如果失败,请中止。
- 解密消息。
其他设计注意事项:
- 永远不要压缩任何东西。密文不可压缩;在加密之前压缩明文会导致信息泄漏(例如TLS上的CRIME和BREACH)。
- 确保使用
mb_strlen()
和mb_substr()
,使用'8bit'
字符集模式以防止出现mbstring.func_overload
问题。
- IVs应该使用CSPRNG生成;如果您正在使用
mcrypt_create_iv()
,请勿使用MCRYPT_RAND
!
- 除非您使用AEAD构造,否则总是先加密然后再加密MAC!
bin2hex()
,base64_encode()
等等可能会通过缓存时间泄露有关您的加密密钥的信息。尽可能避免使用它们。
即使您遵循此处给出的建议,加密也会出错。始终请加密专家检查您的实施。如果您没有幸与本地大学的密码学学生成为私人朋友,则可以随时尝试使用密码学堆栈交换论坛寻求建议。
如果您需要对实现进行专业分析,则始终可以聘请声誉卓著的安全顾问团队来审查您的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更容易
一个我一直在工作的项目是称为加密库岩盐,使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);
我强烈建议您存储一个随机生成的密钥以供长期使用,而不要使用任何形式的密码作为密钥(或派生密钥)。
如果您使用的是Defuse的库:
“但是我真的很想使用密码。”
那是个坏主意,但是好吧,这是安全地做这件事的方法。
首先,生成一个随机密钥并将其存储在常量中。
/**
* 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个字符的密码。您的加密密钥将被可笑地破坏。