I have a simple microsite where a user records a video of themselves using the built-in JS MediaRecorder (https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) and it gets encrypted using openssl_encrypt.
When they want to share/view that video, it’s decrypted using openssl_decrypt.
The problem is, once it comes out the decrypt function the quality has reduced to the point you see artifacts. WebM videos are still watchable, but MP4 videos (captured from iPhone) are unwatchable – the video has 1FPS… What could be causing this dramatic drop in quality?
The solution was working perfectly until we implemented this encryption.
We pass the recorded file into it directly.
encryptFile($_FILES["file"]['tmp_name'], $dir . $filename, 'secret-key');
decryptFile("videos/" . $id . "." . $mime, $decrypted_video, 'secret-key');
encryptFile();
/**
* @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);
}
decryptFile();
/**
* @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);
}
2
Answers
Decryption is not consistent with encryption and determines a wrong IV.
The wrong IVs cause the first block in each decrypted plaintext chunk to be corrupted. The larger the chunksize i.e. the fewer chunks the fewer errors, which explains the improvement when increasing
FILE_ENCRYPTION_BLOCKS
.For encryption and decryption to be consistent, it must be in
decryptFile()
:The code would be more robust if the logic mimicked CBC mode so that decryption were possible without knowing the chunk size. To achieve this
An alternative to avoid padding at all is to use a stream cipher mode like CTR.
OK, I got nerd-sniped by this question and dove in pretty deep.
The summary is that:
FILE_ENCRYPTION_BLOCKS
value, then the IV fix in @Topaco’s answer will likely suffice. Otherwise you will need to apply the rest of the suggested fixes.FILE_ENCRYPTION_BLOCKS
in your existing code likely just moves the compatibility breakage further down the line, possibly past the end of the file, at which point there’s not really any point in streaming encryption code anyway.The simplest answer would be to use an existing library that already does this, and does it well. Namely: jeskew/php-encrypted-streams, which I heavily referrenced to in making my own code work after I hit a wall with it.
The more complex answer is:
$iv_size
bytes of the previous block.Testing code:
Example Output:
It’s worth noting that while
$cipher
in this code seems configurable, this methodology will only work with ciphers in CBC mode, and probably only AES at that. The jeskew/php-encrypted-streams lib linked above implements a wider range of cipher modes, but still only AES.