我一直在使用https://www.php.net/manual/en/function.openssl-encrypt.php上的加密示例(从 CLI)进行实验, 并想知道如何处理较大的文件,例如几 GB 的 SQLite 数据库文件。在同一个 PHP 页面上按照此示例操作之前,我遇到了内存分配错误。
我反应慢,但最终还是让它在 3 GB 的 SQLite 数据库上运行起来。我认为如果设置 options=0,则 base64 是默认值,我尝试使用为简单情况提供的最新示例以及为使用 OPENSSL_RAW_DATA 选项的大型文件提供旧示例。
我的问题是,这篇八年前的文章中给出的方法仍然是正确的方法吗?一些示例给出了 7.1 版本之前和之后的版本。
可以使用加密块的前 16 个字节作为下一个初始化向量吗?
每次加密/解密的块数重要吗?是否应该使用适合最大内存分配的最大大小,还是其他什么?
<?php
//$key should have been previously generated in a cryptographically safe way, like openssl_random_pseudo_bytes
$key = "abcd1234dcba4321";
$cipher = "aes-256-cbc";
$feBlocks = 10000;
$fpPlain = fopen("../Database/filename.db",'rb');
$fpEncrypt = fopen("encrypted.enc", 'wb');
if (in_array($cipher, openssl_get_cipher_methods()))
{
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
// Put the initialzation vector to the beginning of the file
fwrite($fpEncrypt, $iv);
while (!feof($fpPlain)) {
$plaintext = fread($fpPlain, $ivlen * $feBlocks);
$ciphertext = openssl_encrypt($plaintext, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
// Use the first 16 bytes of the ciphertext as the next initialization vector
$iv = substr($ciphertext, 0, $ivlen);
fwrite($fpEncrypt, $ciphertext);
}
fclose($fpPlain);
fclose($fpEncrypt);
}
$fpEncrypt = fopen("encrypted.enc", 'rb');
$fpPlain = fopen("decrypted.db",'wb');
$ivlen = openssl_cipher_iv_length($cipher);
// Get the initialzation vector from the beginning of the file
$iv = fread($fpEncrypt, $ivlen);
while (!feof($fpEncrypt)) {
// we have to read one block more for decrypting than for encrypting
$ciphertext = fread($fpEncrypt, $ivlen * ($feBlocks+1));
$plaintext = openssl_decrypt($ciphertext, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
// Use the first 16 bytes of the ciphertext as the next initialization vector
$iv = substr($ciphertext, 0, $ivlen);
fwrite($fpPlain, $plaintext);
}
fclose($fpEncrypt);
fclose($fpPlain);
文档中的示例
openssl_encrypt()
实现了经过身份验证的加密 (AEAD),一次是使用 HMAC 的 AES/CBC(v7.1 之前,示例 #2),一次是使用 GCM(v7.1 之后,示例 #1),但都是一步加密。除了机密性之外,AEAD 还保证真实性。但是,真正强大的分块AEAD 自定义实现并不简单(有关要求,例如,请参阅Libsodium文档的加密流和文件加密第一部分中的列表),因此最好使用可靠的库。PHP 的一个选项是Sodium,它是Libsodium 的包装器。Libsodium / Sodium有一个流式 API,请参阅加密流和文件加密以及函数。
sodium_crypto_secretstream_...
您的代码(或示例中的底层代码)使用 AES/CBC 实现分块加密。代码可以优化,但原则上是可以的(只要您的要求不需要 AEAD)。
关于优化:当前代码中的加密会填充所有明文块,并使用密文块的第一个块作为下一个明文块的 IV。这意味着在解密过程中必须知道加密的块大小,以便可以重建相同的密文块。此外,中间块的填充效率低下。
将加密和解密的块大小分离会更有意义。如果满足以下条件,则可以实现此目的:
有了这些要求,加密提供的密文与一步加密提供的密文相同,从CBC 图中可以看出。所需的代码更改很少。
有人可能会想到将 MAC 代码扩展为 AEAD。这是可行的,但如上所述,出于安全原因,最好考虑使用已建立的库。
原则上是的,但就上面提到的优化而言,使用密文块的最后一个块作为下一个明文块的 IV 更有意义。
在您的实现中,存在解密的块大小取决于加密的块大小的限制,该限制可以通过优化消除。
除此之外,所选的块大小也会对性能产生影响。我最好使用较大的块大小(但要测试一下)。