I want to import and export an old game file format, and its data is encrypted. Details are found here; shortly summarized, the file is seperated into blocks, each one uses a specific kind of an XOR encryption based on the previous uint, and a checksum trails each block which I'd need to skip when reading data.
Typically, I want to design streams which are laid on the game files to be reusable, and it would be great if there's a stream doing the encryption / decryption in the background, while the developer just works with a BinaryReader/Writer
to do some ReadUInt32()
stuff etc.
So I far I researched that there's a CryptoStream
class in .NET, would the "correct" way to implement en/decryption start with inheriting from that class? I found no articles about someone who tried it that way, thus I'm unsure if I'm completely wrong here.
Whilst not C#, this MSDN page may provide some insight, showing implementation of the ICryptoTransform
interface.
Here's an example of how that might look in C#, with the XOR-with-previous-block you mention in your use case (no doubt you will have to adjust this to match your exact algorithm):
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
class XORCryptoTransform : ICryptoTransform
{
uint previous;
bool encrypting;
public XORCryptoTransform(uint iv, bool encrypting)
{
previous = iv;
this.encrypting = encrypting;
}
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
for (int i = 0; i < inputCount; i+=4)
{
uint block = BitConverter.ToUInt32(inputBuffer, inputOffset + i);
byte[] transformed = BitConverter.GetBytes(block ^ previous);
Array.Copy(transformed, 0, outputBuffer, outputOffset + i, Math.Min(transformed.Length, outputBuffer.Length - outputOffset -i));
if (encrypting)
{
previous = block;
}
else
{
previous = BitConverter.ToUInt32(transformed, 0);
}
}
return inputCount;
}
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
var transformed = new byte[inputCount];
TransformBlock(inputBuffer, inputOffset, inputCount, transformed, 0);
return transformed;
}
public bool CanReuseTransform
{
get { return true; }
}
public bool CanTransformMultipleBlocks
{
get { return true; }
}
public int InputBlockSize
{
// 4 bytes in uint
get { return 4; }
}
public int OutputBlockSize
{
get { return 4; }
}
public void Dispose()
{
}
}
class Program
{
static void Main()
{
uint iv = 0; // first block will not be changed
byte[] plaintext = Guid.NewGuid().ToByteArray();
byte[] ciphertext;
using (var memoryStream = new MemoryStream())
{
using (var encryptStream = new CryptoStream(
memoryStream,
new XORCryptoTransform(iv, true),
CryptoStreamMode.Write))
{
encryptStream.Write(plaintext, 0, plaintext.Length);
}
ciphertext = memoryStream.ToArray();
}
byte[] decrypted = new byte[ciphertext.Length];
using (var memoryStream = new MemoryStream(ciphertext))
using (var encryptStream = new CryptoStream(
memoryStream,
new XORCryptoTransform(iv, false),
CryptoStreamMode.Read))
{
encryptStream.Read(decrypted, 0, decrypted.Length);
}
bool matched = plaintext.SequenceEqual(decrypted);
Console.WriteLine("Matched: {0}", matched);
}
}
In this example, if the input data is a multiple of the block length (4 bytes for uint
in your case), there'll be nothing to be done in TransformFinalBlock
. However, if the data is not a multiple of the block length, the left-over bytes will get handled there.
.NET automatically pads out the array passed to TransformFinalBlock
with zeroes to bring it up to the block-length, but you can detect that by checking the inputCount
(which will be the actual input length, not the padded length) and replacing with your own custom (non-zero) padding if your algorithm calls for it.