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;
}