我有一个简单的微型网站,用户可以在其中使用内置的 JS MediaRecorder (https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) 录制自己的视频并进行加密使用 openssl_encrypt.
当他们想要分享/观看该视频时,它会使用 openssl_decrypt 进行解密。
问题是,一旦出现解密功能,质量就会降低到您看到伪影的程度。 WebM 视频仍然可以观看,但 MP4 视频(从 iPhone 捕获)无法观看 - 视频有 1FPS ......可能导致质量急剧下降的原因是什么?
在我们实施此加密之前,该解决方案一直运行良好。
我们直接把录制的文件传进去
encryptFile($_FILES["file"]['tmp_name'], $dir . $filename, 'secret-key');
decryptFile("videos/" . $id . "." . $mime, $decrypted_video, 'secret-key');
加密文件();
/**
* @param $source Path of the unencrypted file
* @param $dest Path of the encrypted file to created
* @param $key Encryption key
*/
function encryptFile($source, $dest, $key)
{
$cipher = 'aes-256-cbc';
$ivLenght = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivLenght);
$fpSource = fopen($source, 'rb');
$fpDest = fopen($dest, 'w');
fwrite($fpDest, $iv);
while (!feof($fpSource)) {
$plaintext = fread($fpSource, $ivLenght * FILE_ENCRYPTION_BLOCKS);
$ciphertext = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
$iv = substr($ciphertext, 0, $ivLenght);
fwrite($fpDest, $ciphertext);
}
fclose($fpSource);
fclose($fpDest);
}
解密文件();
/**
* @param $source Path of the encrypted file
* @param $dest Path of the decrypted file
* @param $key Encryption key
*/
function decryptFile($source, $dest, $key)
{
$cipher = 'aes-256-cbc';
$ivLenght = openssl_cipher_iv_length($cipher);
$fpSource = fopen($source, 'rb');
$fpDest = fopen($dest, 'w');
$iv = fread($fpSource, $ivLenght);
while (!feof($fpSource)) {
$ciphertext = fread($fpSource, $ivLenght * (FILE_ENCRYPTION_BLOCKS + 1));
$plaintext = openssl_decrypt($ciphertext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
$iv = substr($plaintext, 0, $ivLenght);
fwrite($fpDest, $plaintext);
}
fclose($fpSource);
fclose($fpDest);
}
解密与加密不一致,判断IV错误
错误的 IV 导致每个解密的明文块中的第一个块被损坏。块大小越大,即块越少,错误越少,这解释了增加
FILE_ENCRYPTION_BLOCKS
. 时的改进
要加密解密一致,必须在
decryptFile()
:
$iv = substr($ciphertext, 0, $ivLenght);
通过此更改,解密有效。
如果逻辑模仿 CBC 模式,那么代码会更健壮,这样在不知道块大小的情况下就可以解密(即加密和解密可以使用不同的块大小)。要实现这一点:
只有最后一个明文块必须被填充(当前:所有块都被填充)。修复(在加密示例中,类似于解密示例):
if (!feof($fpSource)) {
$ciphertext = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); // don't pad
} else {
$ciphertext = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv); // pad
}
前一个密文块的最后一个块必须用作IV(当前:使用第一个块)。修正:
$iv = substr($ciphertext, -$ivLenght);
完全避免填充的另一种方法是使用像 CTR 这样的流密码模式(但是,这需要不同的 IV 确定,因为两种模式的 IV 处理不同)。
好吧,我被这个问题搞得一头雾水,陷入了很深的问题。
总结是:
FILE_ENCRYPTION_BLOCKS
值,那么@Topaco 的答案中的 IV 修复可能就足够了。否则,您将需要应用建议的其余修复程序。FILE_ENCRYPTION_BLOCKS
可能只是将兼容性破坏进一步向下移动,可能超过文件末尾,此时流加密代码实际上没有任何意义。最简单的答案是使用一个现有的库,它已经做到了,而且做得很好。即:jeskew/php-encrypted-streams,在我遇到问题后,我在使自己的代码工作时大量引用了它。
更复杂的答案是:
$iv_size
字节。//define('DEBUG', true);
function debug(...$args) {
if( defined('DEBUG') && DEBUG === true ) {
printf(...$args);
}
}
if( ! function_exists('openssl_cipher_block_length') ) {
function openssl_cipher_block_length($cipher) {
return strlen(openssl_encrypt(
"\x00", $cipher, "", OPENSSL_RAW_DATA, str_repeat("\x00", openssl_cipher_iv_length($cipher))
));
}
}
function openssl_encrypt_stream($stream_in, $stream_out, $key, $iv, $cipher, $block_multiplier=256) {
$cipher_block_length = openssl_cipher_block_length($cipher);
$buffer_size = $cipher_block_length * $block_multiplier;
$iv_size = openssl_cipher_iv_length($cipher);
debug("Cipher: %s, Block Size: %d, IV Size: %d, Block Multiplier: %d\n", $cipher, $cipher_block_length, $iv_size, $block_multiplier);
while( ! feof($stream_in) ) {
$buffer = fread($stream_in, $buffer_size);
debug("Plaintext Buffer Length: %d, Content: %s\n", strlen($buffer), bin2hex($buffer));
$options = OPENSSL_RAW_DATA;
if( feof($stream_in) ) {
debug("Final block.\n");
} else {
$options |= OPENSSL_ZERO_PADDING;
}
$e_buffer = openssl_encrypt($buffer, $cipher, $key, $options, $iv);
debug("IV: %s, Ciphertext: %s\n", bin2hex($iv), bin2hex($e_buffer));
if( $e_buffer === false ) {
throw new \Exception(openssl_error_string());
}
$iv = substr($e_buffer, -1 * $cipher_block_length);
fwrite($stream_out, $e_buffer);
debug(PHP_EOL);
}
}
function openssl_decrypt_stream($stream_in, $stream_out, $key, $iv, $cipher, $block_multiplier=256) {
$cipher_block_length = openssl_cipher_block_length($cipher);
$buffer_size = $cipher_block_length * $block_multiplier;
$iv_size = openssl_cipher_iv_length($cipher);
debug("Cipher: %s, Block Size: %d, IV Size: %d, Block Multiplier: %d\n", $cipher, $cipher_block_length, $iv_size, $block_multiplier);
$next_buffer = fread($stream_in, $buffer_size);
do {
$buffer = $next_buffer;
$next_buffer = fread($stream_in, $buffer_size);
debug("Ciphertext Buffer Length: %d, Content: %s\n", strlen($buffer), bin2hex($buffer));
$options = OPENSSL_RAW_DATA;
if( feof($stream_in) && $next_buffer === '' ) {
debug("Final block.\n");
} else {
$options |= OPENSSL_ZERO_PADDING;
}
$p_buffer = openssl_decrypt($buffer, $cipher, $key, $options, $iv);
debug("IV: %s, Plaintext: %s\n", bin2hex($iv), bin2hex($p_buffer));
if( $p_buffer === false ) {
throw new \Exception(openssl_error_string());
}
$iv = substr($buffer, -1 * $cipher_block_length);
fwrite($stream_out, $p_buffer);
debug(PHP_EOL);
} while( !( feof($stream_in) && $next_buffer === '') );
}
测试代码:
// encrypt/decrypt normally as a control
function e_control($file, $key, $iv, $cipher) {
$begin = hrtime(true);
$enc = openssl_encrypt(file_get_contents($file), $cipher, $key, OPENSSL_RAW_DATA, $iv);
$dur = hrtime(true) - $begin;
return [ $enc, $dur ];
}
function d_control($enc, $key, $iv, $cipher) {
$begin = hrtime(true);
$plain = openssl_decrypt($enc, $cipher, $key, OPENSSL_RAW_DATA, $iv);
$dur = hrtime(true) - $begin;
return [ $plain, $dur ];
}
// encrypt test
function e_test($file, $key, $iv, $cipher, $block_multiplier=256) {
$stream_out = fopen('php://memory', 'rwb');
$begin = hrtime(true);
$stream_in = fopen($file, 'rb'); // fairness with control
openssl_encrypt_stream($stream_in, $stream_out, $key, $iv, $cipher, $block_multiplier);
$dur = hrtime(true) - $begin;
fclose($stream_in);
rewind($stream_out);
return [ stream_get_contents($stream_out), $dur ];
}
// decrypt test
function d_test($enc, $key, $iv, $cipher, $block_multiplier=256) {
$stream_in = fopen('php://memory', 'rwb');
$stream_out = fopen('php://memory', 'rwb');
fwrite($stream_in, $enc);
rewind($stream_in);
$begin = hrtime(true);
openssl_decrypt_stream($stream_in, $stream_out, $key, $iv, $cipher, $block_multiplier);
$dur = hrtime(true) - $begin;
fclose($stream_in);
rewind($stream_out);
return [ stream_get_contents($stream_out), $dur ];
}
// dd if=/dev/random of=./test.bin bs=1024 count=10240
$file = 'test.bin';
$cipher = 'aes-256-cbc';
$key = str_repeat("\x00", 32);
$iv = str_repeat("\x00", openssl_cipher_iv_length($cipher));
$mult = 256;
// warm the FS cache for fairness
file_get_contents($file);
list($c, $c_dur) = e_control( $file, $key, $iv, $cipher);
list($e, $e_dur) = e_test( $file, $key, $iv, $cipher, $mult);
list($x, $x_dur) = d_control( $c, $key, $iv, $cipher);
list($d, $d_dur) = d_test( $c, $key, $iv, $cipher, $mult);
printf("Control - Hash: %s, Duration: %0.3f ms\n", md5($c), $c_dur / 1000000);
printf("Encrypt - Hash: %s, Duration: %0.3f ms\n", md5($e), $e_dur / 1000000);
printf("Control - Hash: %s, Duration: %0.3f ms\n", md5($x), $x_dur / 1000000);
printf("Decrypt - Hash: %s, Duration: %0.3f ms\n", md5($d), $d_dur / 1000000);
示例输出:
Control - Hash: 5f5db49554de8fa6a7195c4d0cbc0ad8, Duration: 30.788 ms
Encrypt - Hash: 5f5db49554de8fa6a7195c4d0cbc0ad8, Duration: 75.244 ms
Control - Hash: 8eee2cf0ee0444cafa9280a89821f1ff, Duration: 9.054 ms
Decrypt - Hash: 8eee2cf0ee0444cafa9280a89821f1ff, Duration: 61.600 ms
值得注意的是,虽然此代码中的
$cipher
似乎可配置,但此方法仅适用于 CBC 模式的密码,并且可能仅适用于 AES。上面链接的 jeskew/php-encrypted-streams 库实现了更广泛的密码模式,但仍然只有 AES。