Customisable Save System

Summary

An updated Save System for Unity using simple AES encryption/obfuscation. I had previously implemented a BinaryFormatter but that turned out to be vulnerable to Insecure Deserialisation attacks so this is the updated version.

Allows for encryption and storage of a wide array of data. So long as it can be serialised, it can be saved!
Though it’s not covered in the video, the code also contains things like a custom SetPref/GetPref system, support for multiple files with different data structures, and is just generally very cool!

Script

Full script below (Creative Commons 4.0)

Main DataManager. Can be accessed from any other script. Could easily make the entire class static.

Unlike in the video, the code below uses Newtonsoft’s Json.NET rather than Unity’s Json Utility as it turns out the latter has some limitation when it comes to storing complex data types (like the ones used in the Get/Set Pref methods).

//ArmanDoesStuff 2017
using ArmanDoesStuff.Utilities;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace ArmanDoesStuff.Core
{
    public class DataManager : Object
    {
        [HideInInspector] public const string settingsDataFilename = "/settingsTutData.gd";
        [HideInInspector] public const string settingsPrefsFilename = "/settingsTutPrefs.gd";

        [System.Serializable]
        struct SaveData
        {
            public float userAttribute;
            public string userName;
            public int userLevel;
        }

        public static void SaveSettings(string _reason = "No Reason")
        {
            Debug.Log($"saving settings - {_reason}");
            SaveData saveData;

            //Set values
            Tut_DataUser tut_DataUser = FindObjectOfType<Tut_DataUser>();
            saveData.userAttribute = tut_DataUser.userAttribute;
            saveData.userName = tut_DataUser.userName;
            saveData.userLevel = tut_DataUser.userLevel;

            //Save
            SaveActual<SaveData>(saveData, settingsDataFilename);
        }

        public static void LoadSettings()
        {
            SaveData saveData = LoadActual<SaveData>(settingsDataFilename);

            if (saveData.Equals(default(SaveData)))
            {
                //Create Data
                Debug.Log("Creating data");
                //(1) Sound volumes
                saveData.userAttribute = 0.5f;
                saveData.userName = "New Player";
                saveData.userLevel = 1;
            }

            //Populates the data to later be used
            Tut_DataUser tut_DataUser = FindObjectOfType<Tut_DataUser>();
            tut_DataUser.userAttribute = saveData.userAttribute;
            tut_DataUser.userName = saveData.userName;
            tut_DataUser.userLevel = saveData.userLevel;
        }

        protected static T LoadActual<T>(string _fileName)
        {
            _fileName = Application.persistentDataPath + _fileName;
            if (File.Exists(_fileName))
            {
                byte[] encrypted = File.ReadAllBytes(_fileName);
                string jsonData = EncrypterAES.DecryptStringFromBytes_Aes(encrypted);
                return JsonConvert.DeserializeObject<T>(jsonData);
            }
            return default;
        }

        protected static void SaveActual<T>(T _saveData, string _fileName)
        {
            _fileName = Application.persistentDataPath + _fileName;
            string jsonData = JsonConvert.SerializeObject(_saveData);
            byte[] encrypted = EncrypterAES.EncryptStringToBytes_Aes(jsonData);
            File.WriteAllBytes(_fileName, encrypted);
        }

        //My own version of player prefs - Includes things like default values - Requires .Net 4
        //Uses a Dictionary of Dynamics to store most kinds of data
        public static T GetPref<T>(string _key, T _defaultVal, string _fileName = settingsPrefsFilename)
        {
            Dictionary<string, dynamic> dict = LoadActual<Dictionary<string, dynamic>>(_fileName);
            if (dict == null)
                dict = new Dictionary<string, dynamic>();

            if (dict.ContainsKey(_key))
                return (T)dict[_key];
            return _defaultVal;
        }

        public static void SetPref<T>(string _key, T _value, string _fileName = settingsPrefsFilename)
        {
            Dictionary<string, dynamic> dict = LoadActual<Dictionary<string, dynamic>>(_fileName);
            if (dict == null)
                dict = new Dictionary<string, dynamic>();

            dict[_key] = _value;
            SaveActual(dict, _fileName);
        }

        public static void DeleteFile(string _fileName, bool _quit)
        {
            File.Delete(Application.persistentDataPath + _fileName);
            if (_quit)
                ArmanLibrary.QuitGame();
        }

        public static void ClearSettings()
        {
            Debug.Log("clearing settings");
            DeleteFile(settingsDataFilename, false);
            DeleteFile(settingsPrefsFilename, true);
        }
    }
}

The AES Encrypter/Decrypter used in the LoadActual/SaveActual methods

//ArmanDoesStuff 2021
//https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=net-5.0
using System.IO;
using System.Security.Cryptography;

namespace ArmanDoesStuff.Utilities
{
    public static class EncrypterAES
    {
        // Change this key: https://www.random.org/cgi-bin/randbyte?nbytes=32&format=d
        private static byte[] Key = { 123, 217, 19, 11, 23, 26, 85, 45, 114, 184, 27, 162, 37, 222, 222, 209, 241, 24, 175, 144, 173, 53, 196, 49, 24, 26, 17, 218, 131, 236, 53, 209 };

        // a hardcoded IV should not be used for production AES-CBC code
        // IVs should be unpredictable per ciphertext
        private static byte[] IV = { 116, 64, 191, 111, 23, 3, 113, 119, 231, 121, 152, 112, 79, 32, 114, 166 };

        public static byte[] EncryptStringToBytes_Aes(string plainText)
        {
            // Check arguments.
            if (plainText == null || plainText.Length <= 0)
            {
                UnityEngine.Debug.LogError("plainText invalid");
                return null;
            }
            byte[] encrypted;

            // Create an Aes object with the specified key and IV
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = Key;
                aesAlg.IV = IV;

                // Create an encryptor to perform the stream transform.
                ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

                // Create the streams used for encryption.
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {
                            //Write all data to the stream.
                            swEncrypt.Write(plainText);
                        }
                        encrypted = msEncrypt.ToArray();
                    }
                }
            }

            // Return the encrypted bytes from the memory stream.
            return encrypted;
        }

        public static string DecryptStringFromBytes_Aes(byte[] cipherText)
        {
            // Check arguments.
            if (cipherText == null || cipherText.Length <= 0)
            {
                UnityEngine.Debug.LogError("cipherText invalid");
                return null;
            }

            // Declare the string used to hold
            // the decrypted text.
            string plaintext = null;

            // Create an Aes object with the specified key and IV
            using (Aes aesAlg = Aes.Create())
            {
                aesAlg.Key = Key;
                aesAlg.IV = IV;

                // Create a decryptor to perform the stream transform.
                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                // Create the streams used for decryption.
                using (MemoryStream msDecrypt = new MemoryStream(cipherText))
                {
                    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                        {

                            // Read the decrypted bytes from the decrypting stream
                            // and place them in a string.
                            plaintext = srDecrypt.ReadToEnd();
                        }
                    }
                }
            }

            return plaintext;
        }
    }
}

Requires .NET 4 otherwise you’ll see Missing compiler required member ‘Microsoft.CSharp.RuntimeBinder.Binder.Convert’
Edit > Project Settings > Player > Other Settings > Configuration

An example of how one might save/load and otherwise use the data from the DataManager

//ArmanDoesStuff 2021
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace ArmanDoesStuff.TheThousandRoads
{
    public class Tut_DataUser : MonoBehaviour
    {
        [SerializeField] Slider uiAttribute;
        [SerializeField] TMP_InputField uiName;
        [SerializeField] TMP_Text uiLevel;

        [HideInInspector] public float userAttribute;
        [HideInInspector] public string userName;
        [HideInInspector] public int userLevel;

        public float UserAttribute
        {
            set
            {
                userAttribute = value;
            }
            get
            {
                return userAttribute;
            }
        }
        public string UserName
        {
            set
            {
                userName = value;
            }
            get
            {
                return userName;
            }
        }
        public void Btn_IncreaseLevel()
        {
            userLevel++;
            uiLevel.text = userLevel.ToString();
        }
        public void Btn_SaveData()
        {
            DataManager.SaveSettings();
        }
        public void Btn_LoadData()
        {
            TDataManager.LoadSettings();
            uiAttribute.SetValueWithoutNotify(userAttribute);
            uiName.text = userName;
            uiLevel.text = userLevel.ToString();
        }

        [ContextMenu("Clear Settings")]
        public void ClearSettings()
        {
            DataManager.ClearSettings();
        }
    }
}
Creative Commons License

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.