Search code examples
c#encryptionstreamfile-format

Correct way to implement en/decrypting stream in .NET?


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.


Solution

  • 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.