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

[UnityでセーブデータをSerialize保存する 〜現状〜](http://qiita.com/tempura/items/7e63f39990620189c332)
↑ 現状をまとめた

***

UnityでセーブデータをSerialize化して保存したい！
・・・楽したいので(笑)

- PlayerPrefsは手軽だけど大容量のデータには不向き
- Streamクラスでちまちま1つずつ保存するのはダルい。。
- XmlSerializerは便利だけど中身もろばれ＿|￣|○
- BinaryFormatterはなんかiOSでは動かないとか何とか・・・orz

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

方法としてはObjectをJsonに変換して、それを暗号化して端末に保存/読み込み。
Jsonなら共通の形式に変換出来るし、デバッグ時も分かりやすい。単純な文字列だから暗号化も楽。

# プログラム
```csharp: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](https://qiita-image-store.s3.amazonaws.com/0/96554/9ad7ef2b-6d74-5906-3485-cba5b0fa6d75.png)
# 内容
JsonパーサーはLitJsonを使用してる。別にMiniJsonとか何でもいい。

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

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

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

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

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

2. Jsonで扱えるデータ制限
Jsonで扱えるデータには制限がある。それでなくてもLitJsonとかはデフォルトでlongが使えなかったり。セーブデータを完全に意識しない、ということは出来ず、むしろセーブデータにするようなオブジェクトは専用に意識してデータ構造を作らないといけない。
　
3. 暗号化のコスト
Json同様、暗号化にもコストがかかる。ただでさえデータ量の多いJson文字列を暗号化してるので、セーブするデータが増えれば増えるほどコストが多大にかかる。実際、実環境でまだ試してないので、大丈夫かどうか不安ではある。負荷とか速度とか。

# 最後に
てか特によく調べず作ったけど、今なら何か別の方法があるのかな？便利なAssetとか。
途中まで作ってたから完成させたけど、もう少し何かスマートな形がいいな～