Hi, folks! 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.
- 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();
}
}
- 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!");
}
}
- 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)!
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!