Unityでセーブデータを暗号化してSerialize保存

More than 1 year has passed since last update.

UnityでセーブデータをSerialize保存する 〜現状〜

↑ 現状をまとめた


UnityでセーブデータをSerialize化して保存したい!

・・・楽したいので(笑)


  • PlayerPrefsは手軽だけど大容量のデータには不向き

  • Streamクラスでちまちま1つずつ保存するのはダルい。。

  • XmlSerializerは便利だけど中身もろばれ_| ̄|○

  • BinaryFormatterはなんかiOSでは動かないとか何とか・・・orz

ということで、作ってみた(^^)/

方法としてはObjectをJsonに変換して、それを暗号化して端末に保存/読み込み。

Jsonなら共通の形式に変換出来るし、デバッグ時も分かりやすい。単純な文字列だから暗号化も楽。


プログラム


SceneSample.cs

using UnityEngine;

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using LitJson;

public class SaveData
{
public int Id;
public string Name;
}

public class SceneSample : MonoBehaviour
{
private static readonly string EncryptKey = "c6eahbq9sjuawhvdr9kvhpsm5qv393ga";
private static readonly int EncryptPasswordCount = 16;
private static readonly string PasswordChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static readonly int PasswordCharsLength = PasswordChars.Length;
private static readonly string SavePath = Application.persistentDataPath + "/save.bytes";

void Start()
{
///// 保存 /////////////////////////////////////
{
// セーブデータ作成
SaveData obj = new SaveData();
obj.Id = 1;
obj.Name = "tempura";
Debug.Log("[Encrypt]Id:" + obj.Id);
Debug.Log("[Encrypt]Name:" + obj.Name);

// 暗号化
string json = JsonMapper.ToJson(obj);
string iv;
string base64;
EncryptAesBase64(json, out iv, out base64);
Debug.Log("[Encrypt]json:" + json);
Debug.Log("[Encrypt]base64:" + base64);

// 保存
byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
byte[] base64Bytes = Encoding.UTF8.GetBytes(base64);
using (FileStream fs = new FileStream(SavePath, FileMode.Create, FileAccess.Write))
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(ivBytes.Length);
bw.Write(ivBytes);

bw.Write(base64Bytes.Length);
bw.Write(base64Bytes);
}
}

///// 読み込み /////////////////////////////////////
{
// 読み込み
byte[] ivBytes = null;
byte[] base64Bytes = null;
using (FileStream fs = new FileStream(SavePath, FileMode.Open, FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs))
{
int length = br.ReadInt32();
ivBytes = br.ReadBytes(length);

length = br.ReadInt32();
base64Bytes = br.ReadBytes(length);
}

// 複合化
string json;
string iv = Encoding.UTF8.GetString(ivBytes);
string base64 = Encoding.UTF8.GetString(base64Bytes);
DecryptAesBase64(base64, iv, out json);
Debug.Log("[Decrypt]json:" + json);

// セーブデータ復元
SaveData obj = JsonMapper.ToObject<SaveData>(json);
Debug.Log("[Decrypt]Id:" + obj.Id);
Debug.Log("[Decrypt]Name:" + obj.Name);
}
}

/// <summary>
/// AES暗号化(Base64形式)
/// </summary>
public static void EncryptAesBase64(string json, out string iv, out string base64)
{
byte[] src = Encoding.UTF8.GetBytes(json);
byte[] dst;
EncryptAes(src, out iv, out dst);
base64 = Convert.ToBase64String(dst);
}

/// <summary>
/// AES複合化(Base64形式)
/// </summary>
public static void DecryptAesBase64(string base64, string iv, out string json)
{
byte[] src = Convert.FromBase64String(base64);
byte[] dst;
DecryptAes(src, iv, out dst);
json = Encoding.UTF8.GetString(dst).Trim('\0');
}

/// <summary>
/// AES暗号化
/// </summary>
public static void EncryptAes(byte[] src, out string iv, out byte[] dst)
{
iv = CreatePassword(EncryptPasswordCount);
dst = null;
using (RijndaelManaged rijndael = new RijndaelManaged())
{
rijndael.Padding = PaddingMode.PKCS7;
rijndael.Mode = CipherMode.CBC;
rijndael.KeySize = 256;
rijndael.BlockSize = 128;

byte[] key = Encoding.UTF8.GetBytes(EncryptKey);
byte[] vec = Encoding.UTF8.GetBytes(iv);

using (ICryptoTransform encryptor = rijndael.CreateEncryptor(key, vec))
using (MemoryStream ms = new MemoryStream())
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(src, 0, src.Length);
cs.FlushFinalBlock();
dst = ms.ToArray();
}
}
}

/// <summary>
/// AES複合化
/// </summary>
public static void DecryptAes(byte[] src, string iv, out byte[] dst)
{
dst = new byte[src.Length];
using (RijndaelManaged rijndael = new RijndaelManaged())
{
rijndael.Padding = PaddingMode.PKCS7;
rijndael.Mode = CipherMode.CBC;
rijndael.KeySize = 256;
rijndael.BlockSize = 128;

byte[] key = Encoding.UTF8.GetBytes(EncryptKey);
byte[] vec = Encoding.UTF8.GetBytes(iv);

using (ICryptoTransform decryptor = rijndael.CreateDecryptor(key, vec))
using (MemoryStream ms = new MemoryStream(src))
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
cs.Read(dst, 0, dst.Length);
}
}
}

/// <summary>
/// パスワード生成
/// </summary>
/// <param name="count">文字列数</param>
/// <returns>パスワード</returns>
public static string CreatePassword(int count)
{
StringBuilder sb = new StringBuilder(count);
for (int i = count - 1; i >= 0; i--)
{
char c = PasswordChars[UnityEngine.Random.Range(0, PasswordCharsLength)];
sb.Append(c);
}
return sb.ToString();
}
}


ss.png


内容

JsonパーサーはLitJsonを使用してる。別にMiniJsonとか何でもいい。

Base64の形式に変換をかけているが、特に意味はない。途中まで作ってたソースがそうなってたのでついでにそういう風にしといた。

暗号化時のパスワードは自動で16文字をセーブ処理毎に変化するように処理している。ここらへんは場合によっては変えてもいいかも(サーバ側からパスワード情報をもらう等)。

暗号化方法はAES。C#なら別のやり方もあるかもだけど、とりあえず手軽だったのでこれで。

データ保存は、Json→暗号化→Base64形式→byte情報、を保存している。簡単な暗号化形式なので、出来る限り分かりづらくしてる。あと暗号化/複合化で使用するキー情報も一緒に保存してるが、これは危険なので、基本的には別で管理した方がいいかも。まぁ、めんどくさかったらこのままでも(;^_^A


注意点

これで簡単にセーブデータを保存できる!

・・・とそんな単純な話しではない(笑)


  1. Json変換のオーバヘッド

    まぁ、周知の事実かと思うが、Json変換のオーバヘッドは大きい。データ量多くなるし、端末保存時のコストも上がる。

     


  2. Jsonで扱えるデータ制限

    Jsonで扱えるデータには制限がある。それでなくてもLitJsonとかはデフォルトでlongが使えなかったり。セーブデータを完全に意識しない、ということは出来ず、むしろセーブデータにするようなオブジェクトは専用に意識してデータ構造を作らないといけない。

     


  3. 暗号化のコスト

    Json同様、暗号化にもコストがかかる。ただでさえデータ量の多いJson文字列を暗号化してるので、セーブするデータが増えれば増えるほどコストが多大にかかる。実際、実環境でまだ試してないので、大丈夫かどうか不安ではある。負荷とか速度とか。



最後に

てか特によく調べず作ったけど、今なら何か別の方法があるのかな?便利なAssetとか。

途中まで作ってたから完成させたけど、もう少し何かスマートな形がいいな~