|
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.
原文链接:https://developingsoftware.com/how-to-securely-store-data-in-unity-player-preferences/ |
|