AES decryption doesn't work

Hi, folks! :slightly_smiling_face: I’ve embarked on a journey of developing my first pretty big game, but I stumbled upon something I cannot resolve by myself: a problem with decryption of a save file.

My game encrypts its save files to protect from cheating, as a player might just open an unencrypted file and adjust saved values to gain something on the next saved game loading.

  1. SavingSystem contains a dictionary<string, IEnumerable> that gets filled up or updated with entities states just before saving. It also contains a HashSet of custom types used to encapsulate entities savable data, as it is necessary for data serialization. This metadata gets saved and loaded alongside with a game save file because deserialization without it is not possible.
private void Save()
    {
        using Aes encoder = Aes.Create();

        PrepareSavableData();
        SaveGameData(encoder);
        SaveMetadata(encoder);

        encoder.Clear();

        SavegameCompleted?.Invoke(this, EventArgs.Empty);
    }

    private bool Load()
    {
        using Aes decoder = Aes.Create();

        if (TryLoadMetadata(decoder) && TryLoadGameData(decoder))
        {
            decoder.Clear();

            return true;
        }

        return false;
    }

    private void PrepareSavableData()
    {
        foreach (var entity in _registeredEntities)
        {
            _storedStates[entity.ID] = entity.GetState();
        }
    }
  1. I use DataContractSerializer and custom savable data types where necessary to encapsulate the data that needs to be saved. The serialization part works just fine. I implemented it as follows:
public static Span<byte> Serialize(object input)
    {
        if (input is null)
        {
            throw new ArgumentNullException($"{nameof(input)}", "Cannot serialize an empty imput!");
        }

        using MemoryStream memoryStream = new();
        DataContractSerializer serializer = new(input.GetType());

        serializer.WriteObject(memoryStream, input);
        memoryStream.Position = 0;

        return memoryStream.ToArray();
    }

    public static T Deserialize<T>(Span<byte> input, IEnumerable<Type> knownTypes)
    {
        if (input.IsEmpty || input == null)
        {
            throw new ArgumentNullException($"{nameof(input)}", "Input is empty!");
        }

        if (knownTypes is null)
        {
            throw new ArgumentException("Known types are not supplied! Deserialization will fail!", $"{nameof(knownTypes)}");
        }

        using MemoryStream memoryStream = new(input.Length);
        DataContractSerializer deserializer = new(typeof(T), knownTypes);

        memoryStream.Write(input);
        memoryStream.Position = 0;

        if (deserializer.ReadObject(memoryStream) is T value)
        {
            return value;
        }
        else
        {
            throw new Exception("Input is invalid or corrupted and cannot be restored!");
        }
    }
  1. Now that the data and metadata is gathered it is time to encrypt it with the strongest encryption algorithm currently available - Advanced Encryption Standart (AES)! :sunglasses:
public static Span<byte> Encrypt(Span<byte> input, Aes encryptor, KeyContainer keyContainer)
    {
        if (input.IsEmpty || input == null)
        {
            throw new ArgumentNullException($"{nameof(input)}", "Attempted to encode an empty input!");
        }

        if (encryptor is null)
        {
            throw new ArgumentNullException($"{nameof(encryptor)}", "Encryptor is not provided!");
        }

        if (keyContainer is null)
        {
            throw new ArgumentNullException($"{nameof(keyContainer)}", "Key container is not provided!");
        }

        byte[] currentIV = keyContainer.GetNewIV();

        encryptor.Key = keyContainer.Key;
        encryptor.IV = currentIV;

        using MemoryStream memoryStream = new(input.Length);
        using CryptoStream encryptionStream = new(memoryStream, encryptor.CreateEncryptor(), CryptoStreamMode.Write);
        using BinaryWriter encryptedWriter = new(encryptionStream);
 
        memoryStream.Write(currentIV);
        encryptedWriter.Write(input);
        memoryStream.Position = 0;

        encryptedWriter.Close();
        encryptionStream.Close();

        return memoryStream.ToArray();
    }

    public static Span<byte> Decrypt(Span<byte> input, Aes decryptor, KeyContainer keyContainer)
    {
        if (input.IsEmpty || input == null)
        {
            throw new ArgumentNullException($"{nameof(input)}", "Attempted to decode an empty input!");
        }

        if (decryptor is null)
        {
            throw new ArgumentNullException($"{nameof(decryptor)}", "Decryptor is not provided!");
        }

        if (keyContainer is null)
        {
            throw new ArgumentNullException($"{nameof(keyContainer)}", "Key container is not provided!");
        }

        using MemoryStream memoryStream = new(input.Length);
        memoryStream.Write(input);
        memoryStream.Position = 0;

        var currentIV = new byte[KeyContainer.ByteIVLength];
        memoryStream.Read(currentIV, 0, KeyContainer.ByteIVLength);

        decryptor.Key = keyContainer.Key;
        decryptor.IV = currentIV;

        using CryptoStream decryptionStream = new(memoryStream, decryptor.CreateDecryptor(), CryptoStreamMode.Read);
        using BinaryReader decryptedReader = new(decryptionStream);

        return decryptedReader.ReadBytes(input.Length - KeyContainer.ByteIVLength);
    }

I created KeyContainer scriptable object that stores a unique key and is able to generate a random initialization vector (IV). IV needs to change every single time so the same data gets encrypted differently. I simply put “raw” bytes into a memory stream, encapsulate this stream with the AES encryption stream, and use binary writer to write encoded bytes to the memory stream. I also write current IV to the beginning of the memory stream unencoded, so I can read it later and use to decrypt the data. At last, I return encoded bytes from the memory stream.

However, here comes the problem. Decryption method doesn’t work. In it, at the last code row, an error occurs: CryptographicException: Bad PKCS7 padding. Invalid length 0.

I banging my head against the wall for days trying to find out what to do next, as I’ve tried many recommendations, but nothing works out. Do we have any AES-dudes here? Please, send reinforcements! :cold_face:

Unfortunately, this is one area that is definitely not in my wheelhouse.

Here’s a resource on AES encryption that may help: AES Encryption In C#

I will note that this is actually quite a bit more encryption than you probably need for your game. I’ve outlined a few simpler encryption methods in my Json saving system that while nowhere near as strong as AES, are robust enough to stop garden variety hackers. SavingSystem

Thank you! I will consider Json as an alternative.

This is my third iteration on encrypting game saves. The first was simple XOR ecnryption. The second algorithm I used was DES (data encryption standart). It worked just fine except for one hitch - it is outdated, so any use of it is considered a vulnerability from the Microsoft stand point.

Currently I’ve got several new proposals to check for. I will keep updating this post until my encryption problem is dealt with one way or another for everyone else to learn from my mistakes.

I combine XOR (with a GUID as the key) followed up with a conversion to Base64. This is equivalent to adding a deadbolt to the front door in addition to the lock.

Will it stop the NSA? For about 18 seconds.
Will it stop the Hacker characters shows like Riley on MacGuyver, or Elliot in Mr. Robot? They don’t really exist.
Will it stop a really determined hacker? No, but they’d still need to figure out the GUID for the key, and it would take them more time to decrypt than just playing the game and earning the gold.
Would it have stopped me in college when I used to edit the save files in NetHack and Moria to boost up my gold (showing my age). Yep.

1 Like

Ha, when I was in grad school, those of us sharing an office played Tetris on the office computer. The high scores were “encrypted” with an XOR (combined with bit-reversing everything). A bug in the program meant from time to time a high score of some huge number kept getting added and would eventually dominate the high score list. I wanted to get rid of the bad ones and keep the legit ones, so I looked at the file, figured out how it worked, and adjusted it.

Real encryption would have stopped me dead, though.

Though for single-player games without NFTs or other goodies that mean money, and no internet high score list that a griefer could dominate through hacking, I’d go with the XOR or even nothing at all.

But some Online Poker company learned the hard way that when the stakes are real, make it strong crypto. Someone guessed how the random seed for shuffling was generated (time of day), and was able to win a ton of money before it was stopped.

1 Like

That’s the thing. When an encryption is necessary it means one of two things: its either you need a “cardboard wall painted as a brick one” to fend off occasional cheaters, or a “foolish to even attempt” stronghold. I already know how implement the first one, but still experimenting with the latter option. Will see… :smirk: