e.blog

主にUnity/UE周りのことについてまとめていきます

セーブデータを暗号化して保存する

概要

ゲームには状態の保存など、いわゆる「セーブデータ」が必要なケースが多いです。
今回はそんな「セーブデータ」をシリアライズしたものをバイナリ化し、さらに暗号化して保存する方法を書いておきたいと思います。

なお、今回の実装にあたっては以下の記事を参考にさせていただきました。

qiita.com

developer.wonderpla.net

loumo.jp

使うクラス

セーブデータを暗号化して保存するために、以下のクラス群を利用します。 (もちろん、暗号化には様々なアルゴリズムが存在し、今回紹介する以外の方法でももちろん暗号化を行うことが可能です)

  • System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
  • System.IO.MemoryStream
  • System.IO.FileStream
  • System.Security.Cryptography.MD5CryptoServiceProvider
  • System.Security.Cryptography.RijndaelManaged
  • System.Security.Cryptography.Rfc2898DeriveBytes
  • System.Security.Cryptography.ICryptoTransform

セーブデータをシリアライズしてバイナリデータとしてファイルに保存する

まずは暗号化の話をする前に、セーブデータをシリアライズしてバイナリデータとしてファイルに保存する方法を解説します。

SerializableAttributeでシリアライズ可能なことを明示する

System.SerializableAttributeをclassに指定することでそのクラスがシリアル化可能なことを明示することができます。

ドキュメント↓
docs.microsoft.com

バイナリ化する

データをシリアル化できるよう明示したら、次はそのオブジェクトをバイナリ化します。 バイナリ化にはMemoryStreamBinaryFormatterクラスを使います。

コード断片で示すと以下のようになります。

using (MemoryStream stream = new MemoryStream())
{
    BinaryFormatter formatter = new BinaryFormatter();
    formatter.Serialize(stream, data);

    byte[] source = stream.ToArray();

    using (FileStream fileStream = new FileStream(SavePath, FileMode.Create, FileAccess.Write))
    {
        fileStream.Write(source, 0, source.Length);
    }
}

コード量はそんなに多くないのでぱっと見でなんとなく分かるかと思います。 MemoryStreamを生成し、BinaryFormatterSerializeメソッドを利用してオブジェクトをシリアライズします。 シリアライズしたbyte配列はMemoryStreamに書き込まれます。 結果のbyte配列を取得するにはToArrayメソッドを使います。

そして最後にFileStreamを生成し、byte配列をファイルに書き込みます。 バイナリ化に関してはBinaryFormatterが行ってくれるので、IO周りがしっかり把握できていればさしてむずかしい処理ではないと思います。

データをAESで暗号化する

バイナリデータの保存が分かったところで、次は暗号化についてです。 今回取り上げるのは「AES暗号化」です。

AESとは

AESとは以下の記事から引用させていただくと、

www.atmarkit.co.jp

「AES(Advanced Encryption Standard)」は、DESの後継として米国の国立標準技術研究所(NIST:National Institute of Standards and Technology)によって制定された新しい暗号化規格である。

とのこと。

そしてさらに以下のように続いています。

そして最終的に2001年に「Rijndael(ラインダール)」という暗号化方式が選ばれた。開発者はベルギーの暗号学者、「Joan Daemen(ホァン・ダーメン)」と「Vincent Rijmen(フィンセント・ライメン)」であり、Rijndaelという名称は2人の名前から取られた(とされている)。

実際にコードを見てもらうと分かりますが、実装にはRijndaelManagedというクラスが利用されており、これがまさに上の暗号化方式の名前となっていますね。

暗号化についてのアルゴリズムなどについては上記記事を読んでみるとなんとなく雰囲気は分かるかと思います。
そして完全に余談ですが、以下のiOSアプリが、色々なアルゴリズム(暗号化や証明書などなど)についてアニメーション付きで分かりやすく解説してくれているのでよかったらダウンロードしてみてください。

アルゴリズム図鑑

アルゴリズム図鑑

  • Moriteru Ishida
  • 教育
  • 無料

実装

暗号化に関しては前段で解説した際に行ったバイナリ化したデータ(byte配列)に対して操作を行い、暗号化します。 なので暗号化している部分だけを抜粋してコードを紹介します。 (引数のbyte[] dataは、前段で生成したbyte配列です)

static SaveDataManager()
{
    _rijindeal = new RijndaelManaged();
    _rijindeal.KeySize = 128;
    _rijindeal.BlockSize = 128;

    byte[] bsalt = Encoding.UTF8.GetBytes(_salt);
    Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(_password, bsalt);
    deriveBytes.IterationCount = 1000;

    _rijindeal.Key = deriveBytes.GetBytes(_rijindeal.KeySize / 8);
    _rijindeal.IV = deriveBytes.GetBytes(_rijindeal.BlockSize / 8);
}

static private byte[] Encrypt(byte[] data)
{
    ICryptoTransform encryptor = _rijindeal.CreateEncryptor();
    byte[] encrypted = encryptor.TransformFinalBlock(data, 0, data.Length);

    encryptor.Dispose();

    // Console.WriteLine(string.Join(" ", encrypted));

    return encrypted;
}

static private byte[] Dencrypt(byte[] data)
{
    ICryptoTransform decryptor = _rijindeal.CreateDecryptor();
    byte[] plain = decryptor.TransformFinalBlock(data, 0, data.Length);

    // Console.WriteLine(string.Join(" ", plain));

    return plain;
}

コードの冒頭では静的コンストラクタによってRijndaelManagedオブジェクトを生成しています。

そしてEncryptDencryptメソッドでデータの暗号化、復号化を行っています。 ここでやっていること自体はとてもシンプルですね。 静的コンストラクタで生成したRijndaelManagedオブジェクトから、CreateEncryptorCreateDecryptorをそれぞれ生成し、ICryptorTransformインターフェースのTransformFinalBlockメソッドを実行しているだけです。

戻り値は暗号化、復号化されたbyte配列となります。 あとはこれを、前段のファイル保存の処理で保存してやれば晴れて、セーブデータが暗号化されて保存されたことになります。 (結局のところ、最終的に保存されるのは01で表されるバイナリ表現のデータなので、それ自体が暗号化されているか否かに関わらず、ファイルの保存・読み込みは問題なく行えるというわけですね。(というか、FileStreamはそれを関知しない)

ソースコード

最後に、コンソールアプリとして実行するといくつかの項目を入力するとそれを保存、復元できるものを作ったのでソースコードを載せておきます。
もちろん、Unity上でも動作します。

using System;
using System.IO;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;

[System.Serializable]
public class SaveData
{
    public float Number = 0.5f;
    public string Name = "Hoge";
    public int Count = 5;

    public override string ToString()
    {
        return string.Format("Name: {0}, Number: {1}, Count: {2}", Name, Number, Count);
    }
}

static public class SaveDataManager
{
    public const string SavePath = "./test.bytes";
    private const string _password = "passwordstring";
    private const string _salt = "saltstring";
    static private RijndaelManaged _rijindeal;

    static SaveDataManager()
    {
        _rijindeal = new RijndaelManaged();
        _rijindeal.KeySize = 128;
        _rijindeal.BlockSize = 128;

        byte[] bsalt = Encoding.UTF8.GetBytes(_salt);
        Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(_password, bsalt);
        deriveBytes.IterationCount = 1000;

        _rijindeal.Key = deriveBytes.GetBytes(_rijindeal.KeySize / 8);
        _rijindeal.IV = deriveBytes.GetBytes(_rijindeal.BlockSize / 8);
    }

    static public void Save(SaveData data)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, data);

            byte[] source = stream.ToArray();

            source = AESlize(source);

            using (FileStream fileStream = new FileStream(SavePath, FileMode.Create, FileAccess.Write))
            {
                fileStream.Write(source, 0, source.Length);
            }

            Console.WriteLine("Done [" + data.ToString() + "]");
        }
    }

    static public SaveData Load(string name)
    {
        SaveData data = null;

        using (FileStream stream = new FileStream(name, FileMode.Open, FileAccess.Read))
        {
            using (MemoryStream memStream = new MemoryStream())
            {
                const int size = 4096;
                byte[] buffer = new byte[size];
                int numBytes;

                while ((numBytes = stream.Read(buffer, 0, size)) > 0)
                {
                    memStream.Write(buffer, 0, numBytes);
                }

                byte[] source = memStream.ToArray();
                source = DeAESlize(source);

                using (MemoryStream memStream2 = new MemoryStream(source))
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    data = formatter.Deserialize(memStream2) as SaveData;

                    Console.WriteLine("Loaded.");
                }
            }
        }

        return data;
    }

    static private byte[] AESlize(byte[] data)
    {
        ICryptoTransform encryptor = _rijindeal.CreateEncryptor();
        byte[] encrypted = encryptor.TransformFinalBlock(data, 0, data.Length);

        encryptor.Dispose();

        // Console.WriteLine(string.Join(" ", encrypted));

        return encrypted;
    }

    static private byte[] DeAESlize(byte[] data)
    {
        ICryptoTransform decryptor = _rijindeal.CreateDecryptor();
        byte[] plain = decryptor.TransformFinalBlock(data, 0, data.Length);

        // Console.WriteLine(string.Join(" ", plain));

        return plain;
    }
}

static public class EntryPoint
{
    static public void Main()
    {
        Console.WriteLine("Save? [y/n]");
        string cond = Console.ReadLine();

        if (cond == "y")
        {
            Save();
        }
        else
        {
            Load();
        }
    }

    static void Save()
    {
        Console.WriteLine("Name?");
        string name = Console.ReadLine();

        Console.WriteLine("Number?");
        float number;
        if (!float.TryParse(Console.ReadLine(), out number))
        {
            Console.WriteLine("Must input float value.");
            return;
        }

        Console.WriteLine("Count?");
        int count;
        if (!int.TryParse(Console.ReadLine(), out count))
        {
            Console.WriteLine("Must input int value.");
            return;
        }

        SaveData data = new SaveData
        {
            Name = name,
            Number = number,
            Count = count,
        };
        SaveDataManager.Save(data);
    }

    static void Load()
    {
        SaveData data = SaveDataManager.Load(SaveDataManager.SavePath);
        Console.WriteLine(data.ToString());
    }
}