I'm working on a Unity project where I use AES encryption to secure video files on disk and decrypt them later for streaming through an HTTP server. While implementing chunked decryption, I encountered the following error:
CryptographicException: Bad PKCS7 padding. Invalid length 0.
This error occurs during decryption, specifically when I close the CryptoStream
manually or when it is disposed after usage. If I surround the close operation with a try/catch
, the exception is suppressed, allowing me to generate a decrypted file. However, the resulting file contains artifacts and is thus corrupted.
Here is the method that handles decryption by serving video chunks:
public static int ServeVideoChunk(Stream encryptedStream, Stream outputStream, int offset, int chunk)
{
var position = encryptedStream.Seek(offset, SeekOrigin.Begin);
using var aes = Aes.Create();
aes.Key = Key; // Predefined key (128 bits)
aes.IV = Iv; // Predefined initialization vector (128 bits)
using var cryptoStream = new CryptoStream(encryptedStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
// Align to AES block size
var delta = encryptedStream.Length - position;
var isEOF = chunk > delta;
chunk = (int)(isEOF ? delta : chunk);
chunk -= chunk % (aes.BlockSize / 8);
var buffer = new byte[chunk];
var totalReadBytes = cryptoStream.Read(buffer, 0, chunk);
outputStream.Write(buffer, 0, totalReadBytes);
// Force close the stream to catch the Bad PKCS7 exception and try reading the file at the end
try
{
cryptoStream.Close();
}
catch (Exception e)
{
Debug.Log(e);
}
return totalReadBytes;
}
The Bad PKCS7 padding
exception consistently occurs when closing the CryptoStream
. Without the try/catch
, the exception is thrown at the end of the method after the using
statement for the stream.
Here is how I encrypt the video file. This code works without issues:
public static void EncryptFile(string inputFilePath, string outputFilePath)
{
Debug.Log($"Encrypt InputPath: {inputFilePath} OutputPath: {outputFilePath}");
using (var aes = Aes.Create())
{
aes.Key = Key;
aes.IV = Iv;
using (var fileStream = new FileStream(outputFilePath, FileMode.Create))
using (var cryptoStream = new CryptoStream(fileStream, aes.CreateEncryptor(), CryptoStreamMode.Write))
using (var inputFileStream = new FileStream(inputFilePath, FileMode.Open))
{
inputFileStream.CopyTo(cryptoStream);
}
}
Debug.Log("Encryption complete.");
}
If I decrypt the entire file at once without chunking, I encounter no issues. The decryption completes successfully, and there are no artifacts in the resulting video:
public static void DecryptFile(Stream encryptedStream, Stream outputStream)
{
using var aes = Aes.Create();
aes.Key = Key;
aes.IV = Iv;
using var cryptoStream = new CryptoStream(encryptedStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
cryptoStream.CopyTo(outputStream);
}
However, the above approach does not allow partial reads, and I need chunked decryption for serving videos via an HTTP server.
To simulate the chunked reading and decryption process, I used the following test:
private void Start()
{
var videoPath = UseShortVideo ? ShortVideoPath : VideoPath;
var encryptedVideoPath = UseShortVideo ? ShortEncryptedVideoPath : EncryptedVideoPath;
if (Encrypt)
{
VideoEncryption.EncryptFile(videoPath, encryptedVideoPath);
}
var encryptedVideoFile = new FileInfo(encryptedVideoPath);
using var outputStream = new FileStream($"{encryptedVideoFile.Directory.FullName}/{DecryptedMovieName}",
FileMode.Create, FileAccess.Write);
DecryptByChunk(encryptedVideoPath, outputStream);
}
private static void DecryptByChunk(string inputPath, Stream outputStream)
{
var offset = 0;
var chunk = 2 * 1024 * 1024;
long fileSize;
do
{
using var encryptedStream = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
fileSize = encryptedStream.Length;
int totalBytesRead = VideoEncryption.ServeVideoChunk(encryptedStream, outputStream, offset, chunk);
offset += totalBytesRead;
if (totalBytesRead == 0)
{
Debug.Log($"End of file i: {offset} | total: {fileSize}");
break;
}
Debug.Log($"Offset: {offset} | total: {fileSize}");
} while (offset < fileSize);
}
try/catch
, which allows me to produce a decrypted file, but it contains visible artifacts and is corrupted.How can I reliably decrypt AES-encrypted data in chunks without triggering this exception or corrupting the output?
Solved thanks to @Topaco's comments.
Solved thanks to @Topaco's comments. Here is the updated code with padding disabled for intermediate chunks and previous block IV usage.
public static int ServeVideoChunk(Stream encryptedStream, Stream outputStream, long offset, int chunk)
{
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = Key; // Predefined key (128 bits)
var blockSize = aes.BlockSize / 8; // 16 bytes
// Seek one block before the offset if possible to get the IV
var seekPosition = Math.Max(0, offset - blockSize);
encryptedStream.Seek(seekPosition, SeekOrigin.Begin);
if (seekPosition <= 0)
{
aes.IV = Iv; // Predefined initialization vector (128 bits)
}
else
{
var iv = new byte[blockSize];
encryptedStream.Read(iv, 0, blockSize);
aes.IV = iv;
}
// Remaining ciphertext to decrypt
var delta = encryptedStream.Length - offset;
// EOF
if (chunk > delta)
{
// The delta is supposed to already be block aligned
chunk = (int)delta;
}
else
{
// Disable padding for intermediate chunks
aes.Padding = PaddingMode.None;
}
using var cryptoStream = new CryptoStream(encryptedStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
var buffer = new byte[chunk];
cryptoStream.Read(buffer, 0, chunk);
outputStream.Write(buffer, 0, chunk);
return chunk;
}