Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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とか。
途中まで作ってたから完成させたけど、もう少し何かスマートな形がいいな~

tempura
フリーランスのゲームプログラマー
https://tempura-kingdom.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away