切记,别使用 Mcrypt (包含三个理由和替代方案)
前言
可能你不需要一开始就部署你自己的加密技术,特别是当你还不知道加密技术和身份验证不是一回事的时候。对于生产环境,可以使用 PECL-libsodium 或是 defuse/php 进行加密,这样可以省去很多麻烦。
这篇文章的面向人群是仍然想要编写或者是已经有了自己的加密技术的 php 开发人员。
不使用 Mcrypt 的三大理由
I. Mcrypt 是被废弃的
PHP 的可选扩展 mcrypt
和一个叫作 libmcrypt 的加密库绑定,该库从 2007 年起一直没人维护,尽管有很多 bug,其中一些甚至有补丁可以用。
如果一些小的问题不会成为放弃该库的理由,那么核心的设计缺陷可能会更容易导致编写出的不安全的代码。
II. 令人困惑,且不直观
看一下这个mcrypt 密码列表,然后告诉我你会如何实现 AES-256-CBC
加密。如果你的代码像这样,那么你已经像无头苍蝇一样坠入了 mcrypt 的首要(也是最常见的)设计缺陷。
function encryptOnly($plaintext, $key)
{
$iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $plaintext, MCRYPT_MODE_CBC, $iv);
return $iv.$ciphertext;
}
惊喜吧! MCRYPT_RIJNDAEL_256
并不意味着 AES-256
。
所有 AES 的变体都使用了不同键长度的 128 位块大小。这意味着 MCRYPT_RIJNDAEL_128
是当你想要使用 AES 时的唯一选择。
MCRYPT_RIJNDAEL_192
和 MCRYPT_RIJNDAEL_256
指的是非标准的、研究较少的Rijndael分组密码变体,它在较大的块上运行。
考虑到 AES-256 的密钥调度比 AES-128 差得多,怀疑非标准的 Rijndael 变体可能存在未知弱点并非没有道理,这些弱点在该算法的标准化 128 位块大小版本中不存在。至少,它使得与其他仅实现了 AES 的加密库的互操作变得困难。
mcrypt 让你感到愚蠢,因为你不知道那些你可能真的 不需要 知道的细节。别担心,情况比这还要更糟。
III. 空填充
我们已经说过,不验证你的密文是一个坏主意,平心而论,如果你未能 对 MAC 进行加密,那么无论你选择哪种填充方案,padding oracle attacks(填充提示攻击) 都将成为 CBC(密码块链接)模式下的问题。
如果你用 mcrypt_encrypt()
来加密你的数据,那么你就不得不在编写自己的明文填充策略或使用 mcrypt 的默认实现-零填充-之间做出选择。
为了看看零填充有多糟糕, 让我们用代码示例来展示对一段二进制字符串加密之后再解密的过程 (代码最终运行结果 在这里):
$key = hex2bin('000102030405060708090a0b0c0d0e0f');
$message = hex2bin('5061726101676f6e000300');
$iv = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $message, MCRYPT_MODE_CBC, $iv);
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encrypted, MCRYPT_MODE_CBC, $iv);
// 这里仍旧需要被填充:
var_dump(bin2hex($decrypted));
// 让我们跳过填充:
$stripped = rtrim($decrypted, "\0");
var_dump(bin2hex($stripped));
// 猜猜这里是否与原始数据相等?
var_dump($stripped === $message);
如你所见,用零填充可能会导致数据丢失。一个更安全的选择是使用 PKCS7 填充。
OpenSSL 做得更好
下面是一个未经验证的 AES-256-CBC 加密库的示例,该库是用 Mcrypt 编写的,带有 PKCS7 填充。
/**
* 这个库不安全,因为它在加密后没有加 MAC (Message Authentication Code)
*/
class UnsafeMcryptAES
{
const CIPHER = MCRYPT_RIJNDAEL_128;
public static function encrypt($message, $key)
{
if (mb_strlen($key, '8bit') !== 32) {
throw new Exception("Needs a 256-bit key!");
}
$ivsize = mcrypt_get_iv_size(self::CIPHER);
$iv = mcrypt_create_iv($ivsize, MCRYPT_DEV_URANDOM);
// 增加 PKCS7 填充
$block = mcrypt_get_block_size(self::CIPHER);
$pad = $block - (mb_strlen($message, '8bit') % $block, '8bit');
$message .= str_repeat(chr($pad), $pad);
$ciphertext = mcrypt_encrypt(
MCRYPT_RIJNDAEL_128,
$key,
$message,
MCRYPT_MODE_CBC,
$iv
);
return $iv . $ciphertext;
}
public static function decrypt($message, $key)
{
if (mb_strlen($key, '8bit') !== 32) {
throw new Exception("Needs a 256-bit key!");
}
$ivsize = mcrypt_get_iv_size(self::CIPHER);
$iv = mb_substr($message, 0, $ivsize, '8bit');
$ciphertext = mb_substr($message, $ivsize, null, '8bit');
$plaintext = mcrypt_decrypt(
MCRYPT_RIJNDAEL_128,
$key,
$ciphertext,
MCRYPT_MODE_CBC,
$iv
);
$len = mb_strlen($plaintext, '8bit');
$pad = ord($plaintext[$len - 1]);
if ($pad <= 0 || $pad > $block) {
// 填充错误!
return false;
}
return mb_substr($plaintext, 0, $len - $pad, '8bit');
}
}
这是使用 OpenSSL 编写的库。
/**
* 这个库不安全,因为它在加密后没有加 MAC (Message Authentication Code)
*/
class UnsafeOpensslAES
{
const METHOD = 'aes-256-cbc';
public static function encrypt($message, $key)
{
if (mb_strlen($key, '8bit') !== 32) {
throw new Exception("Needs a 256-bit key!");
}
$ivsize = openssl_cipher_iv_length(self::METHOD);
$iv = openssl_random_pseudo_bytes($ivsize);
$ciphertext = openssl_encrypt(
$message,
self::METHOD,
$key,
OPENSSL_RAW_DATA,
$iv
);
return $iv . $ciphertext;
}
public static function decrypt($message, $key)
{
if (mb_strlen($key, '8bit') !== 32) {
throw new Exception("Needs a 256-bit key!");
}
$ivsize = openssl_cipher_iv_length(self::METHOD);
$iv = mb_substr($message, 0, $ivsize, '8bit');
$ciphertext = mb_substr($message, $ivsize, null, '8bit');
return openssl_decrypt(
$ciphertext,
self::METHOD,
$key,
OPENSSL_RAW_DATA,
$iv
);
}
}
在几乎在各个指标上,openssl 都胜过 mcrypt :
- 指定
'aes-256-cbc'
要比记住使用带有 32 字节二进制密钥的MCRYPT_RIJNDAEL_128
明显得多。 openssl_encrypt()
默认情况下执行 PKCS7 填充,如果有确实需要,也可以指定OPENSSL_ZERO_PADDING
。- 编写的代码更紧凑,可读性更强,出现错误的可能性更低。
- 它执行 AES 加密/解密的速度要快得多,如果处理器支持的话,它可以使用 AES-NI (英特尔高级加密标准指令集)。
AES-NI
还意味着不必担心攻击者从缓存计时信息中恢复你的密钥。 - OpenSSL 正在积极开发和维护。 为了应对去年的 Heartbleed 漏洞,一些组织(包括 Linux 基金会)宣布该项目为关键互联网基础设施,并开始投入大量资源查找和修复系统中的漏洞。如果你仍然不信赖它,还有 LibreSSL 可以选择。
易用性、安全性和性能。除此之外还能有什么要求呢?
然而,OpenSSL 有两个地方是需要留意的。
OpenSSL 陷阱
- 其提供的 CSPRNG 是基于散列函数的用户空间 PRNG,这与 Thomas Ptacek 使用
/dev/urandom
的建议背道而驰。唯一的一种线性替代方法是使用mcrypt_create_iv()
,如上所示,但此函数只有在启用mcrypt
扩展时才能使用。幸运的是,PHP 7 将提供一个核心random_bytes()
函数,该函数使用了内核的 CSPRNG。 - 尽管你的 OpenSSL 版本可能会列出基于 GCM 的密码模式(例如
aes-128-gcm
),但实际上目前 PHP 还不支持这些方法。
总结
不要使用 mcrypt
。 一旦在你的代码中出现 mcrypt
一词,就可能会出错。尽管可以在mcrypt
的基础上提供一个相对安全的加密库( 早期版本的 defuse/php encryption 就是这样做的),但将代码切换到 openssl
能提供更佳的安全性、性能、可维护性和可移植性。
更佳选择:使用 libsodium 替代。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: