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

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

***

[さらに続き](http://qiita.com/tempura/items/84143f36160a66519c5e)。

# MessagePack
MessagePackでいいじゃん！なんで気付かなかったんだろう(笑)

MessagePackは通信データをJsonのような文字列ではなくバイナリでやり取り出来るライブラリ。バイナリなのでデータ量も少なくなるし処理速度もJsonよりも断然速い。Jsonのオーバヘッド→MessagePackでまさに解決。

# MessagePackでセーブデータを暗号化してSerialize保存
```csharp:SceneDemo.cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using MsgPack;

public class SaveData
{
    public int Id;
    public string Name;
    public float[] Param;
    public Chara Chara;
}

public class Chara
{
    public uint Power;
}

public class SceneDemo : 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";

    private static readonly ObjectPacker Packer = new ObjectPacker();

	void Start()
    {
        ///// 保存 /////////////////////////////////////
        {
            // セーブデータ作成
            var obj = new SaveData();
            obj.Id = 1;
            obj.Name = "Volvox";
            obj.Param = new float[] { 0.1f, 0.2f };
            obj.Chara = new Chara() { Power = 530000 };
            Debug.Log("[Save]Id:" + obj.Id);
            Debug.Log("[Save]Name:" + obj.Name);
            Debug.Log("[Save]Parameter:" + obj.Param[0] + "," + obj.Param[1]);
            Debug.Log("[Save]Chara.Power:" + obj.Chara.Power);

            // 暗号化
            byte[] pack = Packer.Pack(obj);
            string iv;
            byte[] data;
            EncryptAes(pack, out iv, out data);

            // 保存
            byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
            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(data.Length);
                bw.Write(data);
            }
        }

        ///// 読み込み /////////////////////////////////////
        {
            // 読み込み
            byte[] ivBytes = null;
            byte[] data = 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();
                data = br.ReadBytes(length);
            }

            // 複合化
            string iv = Encoding.UTF8.GetString(ivBytes);
            byte[] pack;
            DecryptAes(data, iv, out pack);

            // セーブデータ復元
            var obj = Packer.Unpack<SaveData>(pack);
            Debug.Log("[Load]Id:" + obj.Id);
            Debug.Log("[Load]Name:" + obj.Name);
            Debug.Log("[Load]Parameter:" + obj.Param[0] + "," + obj.Param[1]);
            Debug.Log("[Load]Chara.Power:" + obj.Chara.Power);
        }
    }

    /// <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();
    }
}
```
![_work.png](https://qiita-image-store.s3.amazonaws.com/0/96554/a6c8a788-8e0f-0da5-5bcb-3df6bcd64dc7.png)

# 解説
UnityでのMessagePackライブラリは[msgpack-unity](https://github.com/masharada/msgpack-unity)を使用した。

MessagePackでバイナリ化→暗号化→端末保存、を行ってる。ただし、セーブデータの容量が大きくなった時には、バイナリ化→暗号化のコストがでかいので注意が必要。必要な変数のみ暗号化して、それ以外は暗号化せずに普通にバイナリ保存した方が安全な気がする。

MessagePackはLitJsonと違って扱える型に制限がなくて嬉しい♪中見た感じC#の標準プリミティブ型は使えるよう。

# BinaryFormatterとMessagePack
どちらもバイナリ化する点では同じだが、そもそもの用途が違う。BinaryFormatterだったらシリアル化するしないを属性で切り分けたり、データ保存用の処理が行いやすい。対してMessagePackなら通信データと相性がいいので(というかそのためのものだし)、通信データをそのままシリアル化端末保存とかは便利かもしれない(まぁ同じことはBinaryFormatterでも出来るのだけど(笑))。

# 最後に
これでホントのホントにシリアル化周りの話は終了！あとは使う環境によって選定すればよいかと。
しかし、MessagePack便利そうだな～。今度サーバ側で使ってみようかなー？