nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Unity でユーザーデータをJSONにシリアライズして暗号化して保存する

はじめに

  • Unity でユーザーのデータを保存したい場合、最も手軽なのは PlayerPrefs だが、List が使えなかったり平文だったりして不便な面もある。
  • 今回はユーザーデータを LitJSON でシリアライズして、永続データ領域に保存。保存時に暗号化、読み込み時に復号化、といったことを試してみた。
  • 基本的に 【制作実習】Unityでユーザーデータの保存 | ゴゴゴゴ にある通りだが、省略されている部分を補完する。

LitJSON を使って設定データを読み書きする

LitJSON の導入

ラッパークラスの作成

  • LitJSON だと float 型が扱えないとか有るそうなのでラッパーの作成
using LitJson;

// via http://caitsithware.com/wordpress/archives/140
public class JsonMapper
{
	static JsonMapper()
	{
		LitJson.JsonMapper.RegisterExporter<float>( (obj, writer) => { writer.Write( System.Convert.ToDouble( obj ) ); } );
		LitJson.JsonMapper.RegisterImporter<double,float>( (input) => { return System.Convert.ToSingle( input ); } );
		LitJson.JsonMapper.RegisterImporter<System.Int32,long>( (input) => { return System.Convert.ToInt64( input ); } );
	}

	public static T ToObject<T>( string json )
	{
		return LitJson.JsonMapper.ToObject<T>( json );
	}

	public static string ToJson( object obj )
	{
		return LitJson.JsonMapper.ToJson ( obj );
	}
}

設定データのクラスを作成

  • 内容は任意で
using System.Collections;
using System.Collections.Generic;

public class GameData {

	public int level;
	public List<int> characterIds;
}

設定データ読み書き用クラスの作成

using UnityEngine;
using System.IO;

public class GameDataManager {

	static string fileName = "GameData";

	// via http://www.gogogogo.jp/issue/diary/%E4%B8%80%E4%BA%BA%E5%A4%A7%E5%AD%A6/%E5%88%B6%E4%BD%9C%E5%AE%9F%E7%BF%92/1909/
	public static void Save (GameData gameData) {
		string json = JsonMapper.ToJson(gameData);
		string filePath = GetFilePath ();
		FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
		BinaryWriter writer = new BinaryWriter(fileStream);
		writer.Write(json);
		writer.Close();
	}

	// via http://www.atmarkit.co.jp/fdotnet/dotnettips/669bincopy/bincopy.html
	public static GameData Load () {
		string filePath = GetFilePath ();
		FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
		BinaryReader reader = new BinaryReader(fileStream);
		GameData gameData = new GameData ();
		if (reader != null) {
			string str = reader.ReadString();
			gameData = JsonMapper.ToObject<GameData> (str);
			reader.Close();
		}
		return gameData;
	}

	static string GetFilePath () {
		return Application.persistentDataPath + "/" + fileName;
	}
}

読み書きのテスト

  • 下記のスクリプトを任意の GameObject にアタッチして再生して確認する
  • Mac だと ~/Library/Caches/"PlayerSettingsのCompanyName"/"Project名"/GameData に有るファイルからも確認できる
sing UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class GameDataTest : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
		GameData d = new GameData ();
		d.level = 10000;
		d.characterIds = new List<int> { 1, 2, 3 };

		GameDataManager.Save (d);

		GameData d2 = GameDataManager.Load ();
		Debug.Log (d2.level);
	}
}

設定データの暗号化

  • 設定データ読み書き用クラスを編集して、保存されるデータを暗号化する
using UnityEngine;
using System.IO;
using System.Security.Cryptography;

public class GameDataManager {

	static string fileName = "GameData";

	public static void Save (GameData gameData) {
		string json = JsonMapper.ToJson (gameData);
		json += "[END]"; // 復号化の際にPaddingされたデータを除去するためのデリミタの追記
		string crypted = Crypt.Encrypt (json);
		string filePath = GetFilePath ();
		FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
		BinaryWriter writer = new BinaryWriter(fileStream);
		writer.Write(crypted);
		writer.Close();
	}

	public static GameData Load () {
		string filePath = GetFilePath ();
		FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
		BinaryReader reader = new BinaryReader(fileStream);
		GameData gameData = new GameData ();
		if (reader != null) {
			string str = reader.ReadString();
			string decrypted = Crypt.Decrypt (str);
			decrypted = System.Text.RegularExpressions.Regex.Replace (decrypted, @"\[END\].*$", "");
			gameData = JsonMapper.ToObject<GameData> (decrypted);
			reader.Close();
		}
		return gameData;
	}

	static string GetFilePath () {
		return Application.persistentDataPath + "/" + fileName;
	}

	// via http://yukimemo.hatenadiary.jp/entry/2014/04/15/023802
	private class Crypt {

		private const string AesIV  = @"jCddaOybW3zEh0Kl";
		private const string AesKey = @"giVJrbHRlWBDIggF";

		public static string Encrypt( string text ) {

			RijndaelManaged aes = new RijndaelManaged ();
			aes.BlockSize = 128;
			aes.KeySize = 128;
			aes.Padding = PaddingMode.Zeros;
			aes.Mode = CipherMode.CBC;
			aes.Key = System.Text.Encoding.UTF8.GetBytes (AesKey);
			aes.IV = System.Text.Encoding.UTF8.GetBytes (AesIV);

			ICryptoTransform encrypt = aes.CreateEncryptor ();
			MemoryStream memoryStream = new MemoryStream ();
			CryptoStream cryptStream = new CryptoStream (memoryStream, encrypt, CryptoStreamMode.Write);

			byte[] text_bytes = System.Text.Encoding.UTF8.GetBytes (text);

			cryptStream.Write (text_bytes, 0, text_bytes.Length);
			cryptStream.FlushFinalBlock ();

			byte[] encrypted = memoryStream.ToArray ();

			return (System.Convert.ToBase64String (encrypted));
		}

		public static string Decrypt( string cryptText ) {

			RijndaelManaged aes = new RijndaelManaged ();
			aes.BlockSize = 128;
			aes.KeySize = 128;
			aes.Padding = PaddingMode.Zeros;
			aes.Mode = CipherMode.CBC;
			aes.Key = System.Text.Encoding.UTF8.GetBytes (AesKey);
			aes.IV = System.Text.Encoding.UTF8.GetBytes (AesIV);

			ICryptoTransform decryptor = aes.CreateDecryptor ();

			byte[] encrypted = System.Convert.FromBase64String (cryptText);
			byte[] planeText = new byte[encrypted.Length];

			MemoryStream memoryStream = new MemoryStream (encrypted);
			CryptoStream cryptStream = new CryptoStream (memoryStream, decryptor, CryptoStreamMode.Read);

			cryptStream.Read (planeText, 0, planeText.Length);

			return (System.Text.Encoding.UTF8.GetString (planeText));
		}
	}
}