在进一步操作之前,请先了解加密和身份验证之间的区别,以及为什么可能需要经过身份验证的加密,而不仅仅是加密。
为了实现认证加密,你需要加密然后MAC。加密和认证的顺序非常重要!这个问题的一个现有答案犯了这个错误;就像许多用PHP编写的密码库一样。
您应该避免实现自己的密码学,而是使用由密码学专家编写和审查的安全库。
更新:PHP 7.2现在提供libsodium!为了获得最好的安全性,请将您的系统更新为使用PHP 7.2或更高版本,并且只遵循本文中的libsodium建议。
如果您有PECL访问权限,则使用libsodium(如果您想要没有PECL的libsodium,则使用sodium_compat);否则……
使用缓和/ php-encryption;不要滚你自己的密码!
上面链接的两个库使您可以轻松地在自己的库中实现经过身份验证的加密。
如果您仍然想编写和部署您自己的密码学库,与互联网上每个密码学专家的传统智慧相反,那么您必须采取以下步骤。
加密:
CTR模式下使用AES加密。您也可以使用GCM(它消除了对单独MAC的需要)。此外,ChaCha20和Salsa20(由libsodium提供)是流密码,不需要特殊模式。
除非您选择了上面的GCM,否则您应该使用HMAC-SHA-256(或者,对于流密码,使用Poly1305—大多数libsodium api为您做这件事)来验证密文。MAC应该能覆盖IV和密文!
解密:
除非使用Poly1305或GCM,否则请重新计算密文的MAC,并将其与使用hash_equals()发送的MAC进行比较。如果失败,中止。
解密消息。
其他设计考虑因素:
Do not compress anything ever. Ciphertext is not compressible; compressing plaintext before encryption can lead to information leaks (e.g. CRIME and BREACH on TLS).
Make sure you use mb_strlen() and mb_substr(), using the '8bit' character set mode to prevent mbstring.func_overload issues.
IVs should be generating using a CSPRNG; If you're using mcrypt_create_iv(), DO NOT USE MCRYPT_RAND!
Also check out random_compat.
Unless you're using an AEAD construct, ALWAYS encrypt then MAC!
bin2hex(), base64_encode(), etc. may leak information about your encryption keys via cache timing. Avoid them if possible.
即使您遵循这里给出的建议,密码学也会出现很多问题。始终让密码学专家检查您的实现。如果你没有足够的幸运与当地大学的密码学学生成为朋友,你可以尝试密码学堆栈交换论坛寻求建议。
如果您需要对您的实现进行专业分析,您总是可以雇佣一个有信誉的安全顾问团队来检查您的PHP加密代码(披露:我的雇主)。
重要提示:何时不使用加密
不要加密密码。相反,你想使用以下密码哈希算法之一来哈希它们:
Argon2
scrypt
bcrypt
PBKDF2-SHA256, 86,000次迭代
永远不要使用通用哈希函数(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更容易
我一直在做的一个项目是一个名为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处理。
使用化解/php加密的示例
<?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);
我强烈建议只存储一个随机生成的密钥以供长期使用,而不是将任何类型的密码作为密钥(或派生密钥)。
如果你正在使用化解的库:
$string = $keyObject->saveToAsciiSafeString()
$loaded = Key::loadFromAsciiSafeString($string);
“但我真的很想用密码。”
这是个坏主意,但好吧,下面是安全的方法。
首先,生成一个随机键并将其存储在一个常量中。
/**
* 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个字符的密码。你的加密密钥会被滑稽地破解。