.NET Core3.xとUnityでAES暗号化を利用する

タイトルの環境でAES暗号化をしてみたので実装方法の紹介をしたいと思います。

AESの実装方法はサンプルがネットに結構転がっているので簡単に説明するだけでいきます。

いちおう、以前 Cocos2d-x 上で AES 暗号化ライブラリを実装しましたのですが、C++に比べてC#だと実装量が少量 & 環境依存をそこまで考えて処理を分けないで済むので簡単な記述ができます。

確認環境

確認環境は以下の通り

  • Unity 2019.2.17f
  • .NET Core3.1
  • Visual Studio2019
  • Windows 10

(UnityはEditorとWindowsで動作確認済み。Android/iOSの実機動作は未確認、たぶん動くの精神)

AesCypherクラス:AES暗号化の実装

AES暗号化は繰り返し使用する & 少し使い方があったのでAesCypherクラスにライブラリ化しました。

バイト配列のいわゆるバイナリデータを暗号化したバイナリデータに変換と復元を行えます。

サポートする形式は特に指定しなければAES-256, CBC形式で現在ほぼ最強水準のセキュリティとなります。

// AesCypher.cs

using System.Security.Cryptography;

/// <summary>
/// AES暗号化クラス
/// </summary>
public static class AesCypher
{
    /// <summary>
    /// 指定したパラメーターで文字列を暗号化します。
    /// </summary>
    public static byte[] Encrypt(EnctyptionInfo info, byte[] buffer)
    {
        using (var aes = CreateEngine(info))
        {
            ICryptoTransform e = aes.CreateEncryptor();
            return e.TransformFinalBlock(buffer, 0, buffer.Length);
        }
    }

    /// <summary>
    /// 指定したパラメーターで文字列を復号化します。
    /// </summary>
    public static byte[] Decrypt(EnctyptionInfo info, byte[] buffer)
    {
        using (var aes = CreateEngine(info))
        {
            ICryptoTransform e = aes.CreateDecryptor();
            return e.TransformFinalBlock(buffer, 0, buffer.Length);
        }
    }

    /// <summary>
    /// 暗号化を行うためのエンジンを生成します。
    /// </summary>
    public static AesManaged CreateEngine(EnctyptionInfo info)
    {
        return new AesManaged
        {
            KeySize = info.KeySize,
            BlockSize = info.BlockSize,
            Mode = info.Mode,
            IV = info.IV,
            Key = info.Key,
            Padding = info.Padding
        };
    }
}

上記で使用する引数情報を設定するためのデータコンテナです。

// EnctyptionInfo.cs

using System.Security.Cryptography;

/// <summary>
/// AES暗号化を行うときに指定するパラメータを格納するコンテナを表します。
/// </summary>
public class EnctyptionInfo
{
    /// <summary>
    /// 暗号化キーサイズを設定または取得します。256bit固定
    /// </summary>
    public int KeySize { get; } = 256;

    /// <summary>
    /// 暗号化ブロックサイズを設定または取得します。128bit固定
    /// </summary>
    public int BlockSize { get; } = 128;

    /// <summary>
    /// 暗号化モードを取得します。CBC固定。
    /// </summary>
    public CipherMode Mode { get; } = CipherMode.CBC;

    /// <summary>
    /// 初期ベクトルを設定または取得します。
    /// </summary>
    public byte[] IV { get; set; }

    /// <summary>
    /// 暗号化キーを設定または取得します。
    /// </summary>
    public byte[] Key { get; set; }

    /// <summary>
    /// パディングモードを設定または取得します。
    /// </summary>
    public PaddingMode Padding { get; } = PaddingMode.PKCS7;
}

使い方

先にEnctyptionInfoクラスのプロパティにキーと初期ベクトルを設定してAesCypherに引数として指定します。

暗号化・復元のメソッドの第1パラメータに上記コンテナ、第2パラメータに暗号化したいコンテンツのバイナリを指定します。

実際はバイトデータをオブジェクトに相互に変換する処理が必要ですがここでは説明は割愛します。

大抵の場合、JSONで文字列やバイナリのシリアライズからデータを生成しておいて、GetBytesでそのバイナリを取ったりするケースが多いと思います。

// AppMain.cs

using System;
using System.Collections.Generic;
using System.Text;

internal static void Main(string[] args)
{
    // Key(= キー用の)32byte(256bit)のバイナリ
    var key = new List<byte>()
    {
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00
    };

    // IV(= 初期ベクトル用)の16byte(128bit)のバイナリ
    var iv = new List<byte>()
    {
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00
    };

    // 重要:
    // 以下のような文字列から生成すると数値の範囲が狭くなって
    // セキュリティ強度が低下するので必ず上記のようなバイナリの数列を指定してください
    // 
    // 悪い例:
    // byte[] _key = Encoding.ASCII.GetBytes("12345678901234567890123456789012");
    // byte[] _iv = Encoding.ASCII.GetBytes("1234567890");

    // 暗号化の初期情報を設定
    //  → 基本的にKeyとIVだけ設定すればOK
    var encInfo = new EnctyptionInfo
    {
        Key = key.ToArray(),
        IV = iv.ToArray(),
    };

    // 暗号化対象の文字列
    string message = "ああいいううええおおかかききくくけけここ";

    // 暗号化前のデータのバイナリ
    byte[] plane = Encoding.UTF8.GetBytes(message);
    ShowBytes(plane);
    // > 0xE3 0x81 0x82 0xE3 0x81 0x82 0xE3 0x81 0x84 0xE3...(60byte)

    // 文字列を暗号化
    byte[] encrypted = AesCypher.Encrypt(encInfo, plane);
    ShowBytes(encrypted);
    // > 0x90 0xD1 0xEC 0xFE 0x6D 0xD5 0x29 0x79 0x98 0xFD...(64byte)

    // 暗号化を解除
    byte[] decrypt = AesCypher.Decrypt(encInfo, encrypted); // 暗号化と同じ情報で復号化
    ShowBytes(decrypt);
    // > 0xE3 0x81 0x82 0xE3 0x81 0x82 0xE3 0x81 0x84 0xE3...(60byte)

    // 文字列に戻す
    string message2 = Encoding.UTF8.GetString(decrypt);
    Console.WriteLine(message2);
}

// バイト配列の内容をコンソールに表示する
public static void ShowBytes(byte[] bs)
{
    Array.ForEach(bs, b => Console.Write($"0x{b:X2} "));
    Console.WriteLine("");
}

C#だと簡潔に処理が記述できるのでとても良いです。

コメントでも記載していますが、特にリリース時はKEYとIVはは文字列リテラルからGetByteで生成はやめましょう。バイト列の場合も容易に値を知られないように別途仕組みが必要です。

C#のスクリプトにリテラルで書いたりするとせっかく暗号化してもバレバレで一瞬で暗号化が突破されてしまうので気を付けましょう。

関連記事

takap-tech.com

以上です。