在进一步做任何事情之前,请尝试了解加密和身份验证之间的区别,以及为什么您可能需要经过身份验证的加密而不仅仅是加密。
要实现经过身份验证的加密,您需要先加密,然后再加密 MAC。加密和认证的顺序很重要!这个问题的现有答案之一犯了这个错误;就像许多用 PHP 编写的密码库一样。
您应该避免实现自己的密码学,而应使用由密码学专家编写和审查的安全库。
更新:PHP 7.2 现在提供 libsodium!为了获得最佳安全性,请更新您的系统以使用 PHP 7.2 或更高版本,并且仅遵循此答案中的 libsodium 建议。
如果您有 PECL 访问权限,请使用 libsodium (如果您想要没有 PECL 的 libsodium,请使用 sodium_compat);否则...
使用 defuse/php-encryption;不要滚动你自己的密码学!
上面链接的两个库都可以轻松轻松地将经过身份验证的加密实施到您自己的库中。
如果您仍然想编写和部署自己的密码库,违背互联网上每个密码专家的传统智慧,这些是您必须采取的步骤。
加密:
- 在 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()
and mb_substr()
,使用'8bit'
字符集模式来防止出现mbstring.func_overload
问题。
- IV 应该使用CSPRNG生成;如果您正在使用
mcrypt_create_iv()
,请勿使用MCRYPT_RAND
!
- 除非您使用的是 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);
石盐 - Libsodium 变得更容易
我一直在从事的项目之一是一个名为 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);
我强烈建议只存储一个随机生成的密钥以供长期使用,而不是任何类型的密码作为密钥(或派生密钥)。
如果你使用 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 个字符的密码。您的加密密钥将被可笑地破坏。