試作ゲームでユーザーデータをローカルにファイル保存することにしたため、そのためのクラスを作ってみました。暗号化なし版と暗号化あり版です。
インスタンスをシリアライズし → 圧縮し → (暗号化し → )ファイルに保存 します。
ちなみに公式のPlayerPrefsを使わなかったのは、オブジェクトをまるっと保存する用途には向かなさそうに思えたためです。
事前準備
GZip圧縮用に、DotNetZipのライブラリIonic.Zlib.CF.dll を、Asset以下の適当なディレクトリに保存しておきます。
今回は \DotNetZipLib-DevKit-v1.9\zlib-v1.9-CompactFramework\Release\Ionic.Zlib.CF.dll を利用しました。
暗号化なし版
まずは暗号化なし版から記載します。
インスタンスのシリアライズ・圧縮・保存を行うクラスです。
using System;
using System.Text;
using System.IO;
using UnityEngine;
using Ionic.Zlib;
namespace Example
{
public class DataFilePlain : IDataFile
{
// constant
readonly string PathFormat = Application.persistentDataPath + "/{0}.json.gz";
readonly int BufferSize = 1024;
// 保存
public void Save<T>(T instance, string dataName)
{
var filepath = String.Format(PathFormat, dataName);
// instance → JSON
var json = JsonUtility.ToJson(instance);
using (var outputStream = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.None)) // 圧縮データ → バイナリファイル
using (var compressStream = new GZipStream(outputStream, CompressionMode.Compress)) // MemoryStream → 圧縮データ
using (var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(json))) // Json → MemoryStream
{
int byteCount;
byte[] buffer = new byte[BufferSize];
while ((byteCount = inputStream.Read(buffer, 0, buffer.Length)) > 0)
compressStream.Write(buffer, 0, byteCount);
}
}
// 読み込み
public T Load<T>(string dataName)
{
var filepath = String.Format(PathFormat, dataName);
if (!File.Exists(filepath))
return default(T);
using (var inputStream = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.None)) // バイナリファイル → 圧縮データ
using (var compressStream = new GZipStream(inputStream, CompressionMode.Decompress))
using (var outputStream = new MemoryStream())
{
// 圧縮データ → MemoryStream
int byteCount;
byte[] buffer = new byte[BufferSize];
while ((byteCount = compressStream.Read(buffer, 0, buffer.Length)) > 0)
outputStream.Write(buffer, 0, byteCount);
// MemoryStream → Json
var json = Encoding.UTF8.GetString(outputStream.ToArray());
// JSON → instance
T instance = JsonUtility.FromJson<T>(json);
return instance;
}
}
}
}
チートしたくば存分にするがよいと言わんばかりのファイル名となりますが、開発中はデバッグしやすいこちらのほうが楽と思われます。
また、あとで差し替えるため、インターフェイスを切っています。それがこちら。
namespace Example
{
public interface IDataFile
{
void Save<T>(T instance, string dataName);
T Load<T>(string dataName);
}
}
暗号化あり版
次に、上述のコードに暗号化を加えたバージョンがこちらです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using UnityEngine;
using Ionic.Zlib;
using System.Security.Cryptography;
namespace Example
{
public class DataFileEncrypting : IDataFile
{
// constant
readonly string PathFormat = Application.persistentDataPath + "/{0}.dat";
readonly int BufferSize = 1024;
readonly string Password = "password"; // TODO 本番では変更する
readonly int AesBlockSize = 128;
readonly int AesKeySize = 128;
readonly int KeySize = 16;
// 保存
public void Save<T>(T instance, string dataName)
{
var filepath = String.Format(PathFormat, dataName);
// instance → JSON
var json = JsonUtility.ToJson(instance);
// @see http://qiita.com/hibara/items/c9096376b1d7b5c8e2ae
// @see https://github.com/hibara/FileEncryptSample/blob/master/FileEncryptSample/Form1.cs
using (var aes = new AesManaged())
{
aes.BlockSize = AesBlockSize;
aes.KeySize = AesKeySize;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(Password, 16);
byte[] salt = new byte[KeySize];
salt = deriveBytes.Salt;
byte[] bufferKey = deriveBytes.GetBytes(KeySize);
aes.Key = bufferKey;
aes.GenerateIV();
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (var outputStream = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.None)) // 暗号化データ → バイナリファイル
using (var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)) // 圧縮データ → 暗号化データ
using (var compressStream = new GZipStream(cryptoStream, CompressionMode.Compress)) // MemoryStream → 圧縮データ
using (var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(json))) // Json → MemoryStream
{
outputStream.Write(salt, 0, KeySize);
outputStream.Write(aes.IV, 0, KeySize);
// MemoryStream → GZipファイル
int byteCount;
byte[] buffer = new byte[BufferSize];
while ((byteCount = inputStream.Read(buffer, 0, buffer.Length)) > 0)
compressStream.Write(buffer, 0, byteCount);
}
}
}
// 読み込み
public T Load<T>(string dataName)
{
var filepath = String.Format(PathFormat, dataName);
if (!File.Exists(filepath))
return default(T);
// @see http://qiita.com/hibara/items/c9096376b1d7b5c8e2ae
// @see https://github.com/hibara/FileEncryptSample/blob/master/FileEncryptSample/Form1.cs
using (var aes = new AesManaged())
// バイナリファイル → 暗号化データ
using (var inputStream = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.None))
{
aes.BlockSize = AesBlockSize;
aes.KeySize = AesKeySize;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
byte[] salt = new byte[KeySize];
inputStream.Read(salt, 0, KeySize);
byte[] iv = new byte[KeySize];
inputStream.Read(iv, 0, KeySize);
aes.IV = iv;
Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(Password, salt);
byte[] bufferKey = deriveBytes.GetBytes(KeySize);
aes.Key = bufferKey;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (var cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read)) // 暗号化データ → 圧縮データ
using (var compressStream = new GZipStream(cryptoStream, CompressionMode.Decompress))
using (var outputStream = new MemoryStream())
{
// 圧縮データ → MemoryStream
int byteCount;
byte[] buffer = new byte[BufferSize];
while ((byteCount = compressStream.Read(buffer, 0, buffer.Length)) > 0)
outputStream.Write(buffer, 0, byteCount);
// MemoryStream → Json
var json = Encoding.UTF8.GetString(outputStream.ToArray());
// JSON → instance
T instance = JsonUtility.FromJson<T>(json);
return instance;
}
}
}
}
}
尚、暗号化の箇所は下記の記事とサンプルを参考にさせて頂きました。
- http://qiita.com/hibara/items/c9096376b1d7b5c8e2ae
- https://github.com/hibara/FileEncryptSample/blob/master/FileEncryptSample/Form1.cs
テストコード
最後に、動作確認に使ったテストコードがこちらとなります。
Windows上のUnityで、Editor Tests Runnerから動作させました。
using NUnit.Framework;
using Example;
using System;
using System.Collections.Generic;
public class DataFileTest {
[Serializable]
public class TestObject
{
public int Id;
public string Name;
public float Rate;
public List<TestChild> ChildList;
}
[Serializable]
public class TestChild
{
public int ChildId;
}
[Test]
public void EditorTest() {
var child1 = new TestChild();
child1.ChildId = 10;
var child2 = new TestChild();
child2.ChildId = 101;
var instance = new TestObject();
instance.Id = 1;
instance.Name = "テスト";
instance.Rate = 10.5f;
instance.ChildList = new List<TestChild>() { child1, child2 };
var file = new DataFilePlain();
file.Save(instance, "test");
TestObject testObject;
testObject = file.Load<TestObject>("test");
Assert.AreEqual(testObject.Id, instance.Id);
Assert.AreEqual(testObject.Name, instance.Name);
Assert.AreEqual(testObject.Rate, instance.Rate);
Assert.AreEqual(testObject.ChildList[0].ChildId, instance.ChildList[0].ChildId);
Assert.AreEqual(testObject.ChildList[1].ChildId, instance.ChildList[1].ChildId);
var fileEncrypting = new DataFileEncrypting();
fileEncrypting.Save(instance, "test");
testObject = fileEncrypting.Load<TestObject>("test");
Assert.AreEqual(testObject.Id, instance.Id);
Assert.AreEqual(testObject.Name, instance.Name);
Assert.AreEqual(testObject.Rate, instance.Rate);
Assert.AreEqual(testObject.ChildList[0].ChildId, instance.ChildList[0].ChildId);
Assert.AreEqual(testObject.ChildList[1].ChildId, instance.ChildList[1].ChildId);
}
}
以上。