しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Unity で MessagePack for C# を利用する(暗号化)

※ これは 2022/02/03 時点の Unity 2021.2.10f1、MessagePack for C# v2.3.85 の情報です

最新版では動作が異なる可能性がありますのでご注意ください

前回、MessagePack for C# のファイルへの読み書きができたので、さらに暗号化・復号化を組み合わせて堅牢にしてみる

とりあえず下記の ICryptPersistentData とその拡張クラスを用意

CryptIVCryptKey は適当な半角16文字の文字列を指定する

using Cysharp.Threading.Tasks;
using MessagePack;
using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;

namespace MatatabiUx.Common
{
    /// <summary>
    /// 暗号化する永続化データのインタフェース
    /// </summary>
    public interface ICryptPersistentData {}

    /// <summary>
    /// 暗号化する永続化データを処理する拡張クラス
    /// </summary>
    public static class ICryptPersistentDataExtentions
    {
        /// <summary>
        /// 暗号化 IV
        /// </summary>
        private const string CryptIV = @"matatabi_cat_eye";

        /// <summary>
        /// 暗号化 Key
        /// </summary>
        private const string CryptKey = @"cafe_mocha_choco";

        /// <summary>
        /// 暗号化アルゴリズム
        /// </summary>
        /// <param name="CreateAlgorithm()"></param>
        /// <returns></returns>
        public static SymmetricAlgorithm Algorithm = algorithm ?? (algorithm = CreateAlgorithm());
        private static SymmetricAlgorithm algorithm = null;

        private static SymmetricAlgorithm CreateAlgorithm() =>
            new RijndaelManaged()
            {
                BlockSize = 128,
                KeySize = 128,
                Padding = PaddingMode.Zeros,
                Mode = CipherMode.CBC,
                Key = System.Text.Encoding.UTF8.GetBytes(CryptKey),
                IV = System.Text.Encoding.UTF8.GetBytes(CryptIV),
            };

        /// <summary>
        /// 暗号化器
        /// </summary>
        private static ICryptoTransform Encryptor = encryptor ?? (encryptor = Algorithm.CreateEncryptor());
        private static ICryptoTransform encryptor = null;

        /// <summary>
        /// 復号化器
        /// </summary>
        private static ICryptoTransform Decryptor = decryptor ?? (decryptor = Algorithm.CreateDecryptor());
        private static ICryptoTransform decryptor = null;

        /// <summary>
        /// ファイルから暗号化データを読み込む
        /// </summary>
        /// <param name="data">読み込むデータモデル</param>
        /// <param name="path">読み込み先パス</param>
        /// <typeparam name="T">読み込むデータモデルの型</typeparam>
        /// <returns>読み込んだデータ、ファイルがなかった場合はデフォルト値</returns>
        public static async UniTask<T> LoadAsync<T>(this T data, string path) where T : ICryptPersistentData, new()
        {
            if (!File.Exists(path))
            {
                return new T();
            }
            try
            {
                using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
                    using (var cryptStream = new CryptoStream(fileStream, Decryptor, CryptoStreamMode.Read))
                        return await MessagePackSerializer.DeserializeAsync<T>(cryptStream);
            }
            catch (IOException) { }
            catch (UnauthorizedAccessException) { }
            catch (SecurityException) { }

            return default;
        }

        /// <summary>
        /// ファイルに暗号化データを書き込む
        /// </summary>
        /// <param name="data">書き込むデータモデル</param>
        /// <param name="path">書き込み先パス</param>
        /// <typeparam name="T">書き込むデータモデルの型</typeparam>
        /// <returns>書き込みが成功した場合は true, それ以外は false</returns>
        public static async UniTask<bool> SaveAsync<T>(this T data, string path) where T : ICryptPersistentData, new()
        {
            try
            {
                using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
                    using (var cryptStream = new CryptoStream(fileStream, Encryptor, CryptoStreamMode.Write))
                        await MessagePackSerializer.SerializeAsync(cryptStream, data);
                return true;
            }
            catch (IOException) { }
            catch (UnauthorizedAccessException) { }
            catch (SecurityException) { }

            return false;
        }
    }
}

次に ICryptPersistentData を継承した CryptData.cs を作る

SaveData.cs で記述した内容とあまり変わらず、Message というデータだけ持たせた

using Cysharp.Threading.Tasks;
using MessagePack;
using System;
using System.IO;
using UnityEngine;

namespace MatatabiUx.Common
{
    /// <summary>
    /// 暗号化データ
    /// </summary>
    [MessagePackObject]
    [Serializable]
    public partial class CryptData : ICryptPersistentData
    {
        /// <summary>
        /// Message
        /// </summary>
        /// <value></value>
        [Key(0)]
        public string Message { get; set; } = default;

        public CryptData()
        {
        }

        [SerializationConstructor]
        public CryptData(string message)
        {
            this.Message = message;
        }

        
        #region Singleton

        protected static CryptData Instance => instance
            ?? (instance = new CryptData(Path.Combine(Application.persistentDataPath, "crypt.dat"), ""));
        private static CryptData instance = null;

        #endregion //Singleton

        #region File IO

        protected static string FilePath = null;

        protected CryptData(string filePath, string message)
        {
            FilePath = filePath;
        }

        /// <summary>
        /// データを読み込む
        /// </summary>
        /// <typeparam name="CryptData">暗号化データ</typeparam>
        /// <returns>読み込んだデータ</returns>
        public static UniTask<CryptData> LoadAsync() => Instance.LoadAsync<CryptData>(FilePath);

        /// <summary>
        /// データを書き込む
        /// </summary>
        /// <typeparam name="CryptData">暗号化データ</typeparam>
        /// <returns>書き込みが成功した場合は true, それ以外は false</returns>
        public UniTask<bool> SaveAsync()
        {
            return this.SaveAsync<CryptData>(FilePath);
        }

        #endregion //File IO
   }
}

新しくデータ型を追加したので、下記の記事でやったように Serializer のコード再生成

https://www.matatabi-ux.com/entry/2022/02/03/100000

最後にテストスクリプトを書き換えて実行してみる

暗号化データを利用して読み書きしてるだけ

using MatatabiUx.Common;
using UnityEngine;

public class Test : MonoBehaviour
{
    public void Awake()
    {
        // シリアライザの初期設定
        MessagePack.Resolvers.StaticCompositeResolver.Instance.Register(
            MessagePack.Resolvers.GeneratedResolver.Instance, // コード生成した型解決クラス
            MessagePack.Unity.UnityResolver.Instance,
            MessagePack.Unity.Extension.UnityBlitWithPrimitiveArrayResolver.Instance,
            MessagePack.Resolvers.StandardResolver.Instance
        );
        var option = MessagePack.MessagePackSerializerOptions.Standard
            .WithCompression(MessagePack.MessagePackCompression.Lz4BlockArray) // LZ4 圧縮利用
            .WithResolver(MessagePack.Resolvers.StaticCompositeResolver.Instance);
        MessagePack.MessagePackSerializer.DefaultOptions = option;
    }

    public async void Start()
    {
        var data = await CryptData.LoadAsync();
        data.Message = "ラグドールもふもふ巨大ねこ";

        // ファイルに書き込み
        await data.SaveAsync();

        // ファイルから読み込み
        data = await CryptData.LoadAsync();

        UnityEngine.Debug.Log(
            $"message={data.Message}");
    }
}

ログを見ると正しく読み込めている様子

復号化成功

書き込んだ crypt.dat を開いて中身を確認すると

���"F�6'�|�ő'f��� ���ur[�i�͹)b�j���zY�}!�q���k����9B��fq\�

全然読めない・・・平文ではなくちゃんと暗号化されたっぽい