• Home
  • Tutorials
  • Development Tools
  • Contact Us

Developing Software

Mastering Software Craftsmanship

Unity 3D: How to Secure Your Player Preferences

1st May 2014 by @developingsoft

Have you ever wondered why people seem to get really high scores on the iOS leaderboards? Its probably because the developer forgot to encrypt their Unity player preferences. In this article I will show you a simple way to encrypt the player preferences so that users can’t cheat or bypass in-app purchases.

Unity PlayerPrefs Screenshot

Figure 1: A before and after of Unity player preferences on iOS

AES, DES or 3DES Encryption?

The most secure encryption algorithm today is AES 256. For the purpose of encrypting the player preferences I recommend DES. The reason is because it means you don’t have to get an export license when submitting your game to iTunes.

The Code

In this example there are two classes. DESEncryption for handling the Encrypt/Decrypt functionality and SecurePlayerPrefs which is a secure implementation of the Unity engines built in PlayerPrefs.

Encryption Class

using System;
using System.Security.Cryptography;
using System.Text;
using System.IO;

public interface IEncryption
{
    string Encrypt(string plainText, string password);
    bool TryDecrypt(string cipherText, string password, out string plainText);
}

public class DESEncryption : IEncryption
{
    const int Iterations = 1000;

    public string Encrypt(string plainText, string password)
    {
        if (plainText == null)
        {
            throw new ArgumentNullException("plainText");
        }

        if (string.IsNullOrEmpty(password))
        {
            throw new ArgumentNullException("password");
        }

        // create instance of the DES crypto provider
        var des = new DESCryptoServiceProvider();

        // generate a random IV will be used a salt value for generating key
        des.GenerateIV();

        // use derive bytes to generate a key from the password and IV
        var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, des.IV, Iterations);

        // generate a key from the password provided
        byte[] key = rfc2898DeriveBytes.GetBytes(8);

        // encrypt the plainText
        using (var memoryStream = new MemoryStream())
        using (var cryptoStream = new CryptoStream(memoryStream, des.CreateEncryptor(key, des.IV), CryptoStreamMode.Write))
        {
            // write the salt first not encrypted
            memoryStream.Write(des.IV, 0, des.IV.Length);

            // convert the plain text string into a byte array
            byte[] bytes = Encoding.UTF8.GetBytes(plainText);

            // write the bytes into the crypto stream so that they are encrypted bytes
            cryptoStream.Write(bytes, 0, bytes.Length);
            cryptoStream.FlushFinalBlock();

            return Convert.ToBase64String(memoryStream.ToArray());
        }
    }

    public bool TryDecrypt(string cipherText, string password, out string plainText)
    {
        // its pointless trying to decrypt if the cipher text
        // or password has not been supplied
        if (string.IsNullOrEmpty(cipherText) || 
            string.IsNullOrEmpty(password))
        {
            plainText = "";
            return false;
        }

        try
        {   
            byte[] cipherBytes = Convert.FromBase64String(cipherText);

            using (var memoryStream = new MemoryStream(cipherBytes))
            {
                // create instance of the DES crypto provider
                var des = new DESCryptoServiceProvider();

                // get the IV
                byte[] iv = new byte[8];
                memoryStream.Read(iv, 0, iv.Length);

                // use derive bytes to generate key from password and IV
                var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, iv, Iterations);

                byte[] key = rfc2898DeriveBytes.GetBytes(8);

                using (var cryptoStream = new CryptoStream(memoryStream, des.CreateDecryptor(key, iv), CryptoStreamMode.Read))
                using (var streamReader = new StreamReader(cryptoStream))
                {
                    plainText = streamReader.ReadToEnd();
                    return true;
                }
            }
        }
        catch(Exception ex)
        {
            // TODO: log exception
            Console.WriteLine(ex);

            plainText = "";
            return false;
        }
    }
}

The above DESEncryption class is a DES implementation that has two methods. Encrypt and TryDecrypt. The Encrypt method generates a random IV so that the data stored on disk is always different, even if the data to encrypt is the same. It does this by generating a random key from the IV and user supplied password. The IV is then stored with the encrypted data so that it can be used by the TryDecrypt function.

SecurePlayerPrefs

using UnityEngine;
using MadPike.Security;
using System.Security.Cryptography;
using System.Text;

public static class SecurePlayerPrefs
{
    public static void SetString(string key, string value, string password)
    {
        var desEncryption = new DESEncryption();
        string hashedKey = GenerateMD5(key);
        string encryptedValue = desEncryption.Encrypt(value, password);
        PlayerPrefs.SetString(hashedKey, encryptedValue);
    }

    public static string GetString(string key, string password)
    {
        string hashedKey = GenerateMD5(key);
        if (PlayerPrefs.HasKey(hashedKey))
        {
            var desEncryption = new DESEncryption();
            string encryptedValue = PlayerPrefs.GetString(hashedKey);
            string decryptedValue;
            desEncryption.TryDecrypt(encryptedValue, password, out decryptedValue);
            return decryptedValue;
        }
        else
        {
            return "";
        }
    }

    public static string GetString(string key, string defaultValue, string password)
    {
        if (HasKey(key))
        {
            return GetString(key, password);
        }
        else
        {
            return defaultValue;
        }
    }

    public static bool HasKey(string key)
    {
        string hashedKey = GenerateMD5(key);
        bool hasKey = PlayerPrefs.HasKey(hashedKey);
        return hasKey;
    }

    /// <summary>
    /// Generates an MD5 hash of the given text.
    /// WARNING. Not safe for storing passwords
    /// </summary>
    /// <returns>MD5 Hashed string</returns>
    /// <param name="text">The text to hash</param>
    static string GenerateMD5(string text)
    {
        var md5 = MD5.Create();
        byte[] inputBytes = Encoding.UTF8.GetBytes(text);
        byte[] hash = md5.ComputeHash(inputBytes);

        // step 2, convert byte array to hex string
        var sb = new StringBuilder();
        for (int i = 0; i < hash.Length; i++)
        {
            sb.Append(hash[i].ToString("X2"));
        }
        return sb.ToString();
    }
}

The above SecurePlayerPrefs class shows the implementation of SetString and GetString. I will leave it to you to implement the remaining methods like GetFloat, SetFloat. This can be done quite easily by converting the value and storing it with the SetString method.

One thing worth mentioning about this class is the GenerateMD5 function. This is used to hash the key for each value so that if the user opens the player preferences file they will not be able to see a user friendly name of what the encrypted data is.

Example

Below is an example of the SecurePlayerPrefs in action:

using UnityEngine;
using System.Collections;

public class SecurePlayerPrefsTest : MonoBehaviour 
{
    void Start () 
    {
        SecurePlayerPrefs.SetString("HelloKey", "Hello World", "password"); 
        PlayerPrefs.Save();


        string helloWorld = SecurePlayerPrefs.GetString("HelloKey", "password");
        Debug.Log(helloWorld);
    }
}

Conclusion

With the above code you should easily be able to encrypt your data stored in the player preferences. I would recommend still using the Unity PlayerPrefs for things that are not sensitive because it is quicker than using encryption.

The other thing I would say, is the data is never completely secure but it does make it harder for the majority of people to hack. Just remember to use a good password and obscure it somehow.

Share this on:

Filed Under: Tutorials Tagged With: C#, Unity

Search

Advertisement

Newsletter

Subscribe now to receive practical tips on how to become a better software developer.

Free - No Spam - 100% Email Privacy

Featured Posts

Abstract Factory Pattern: C# Example Using the Unity Game Engine

23 Software Design Patterns That Will Make You a More Effective Programmer

How to Deploy an ASP.NET Core Website to Ubuntu with Git

How to Run an ASP.NET Core Website in Production on Ubuntu Linux

How to Install the Edimax Wireless nano USB Adapter on Windows IoT Core for Raspberry Pi

How to Convert a Post Title into a Friendly URL (Slug) in C#

How to Convert Markdown to HTML in ASP.NET Core

How to Send an E-Mail with ASP.NET Core and Mailgun

How to Generate a Sitemap in ASP.NET MVC and ASP.NET Core

How to Create an MD5 Hash of a String in C# and Displaying a Gravatar Image

© 2014–2025 Developing SoftwareTerms • Privacy