🔐

【C#】AES を使用した暗号化と復号の手順

2023/10/31に公開

ログインパスワードなど、そのままの状態でネットワーク上に流してはいけないものについては、暗号化して安全にクライアントとサーバー間で通信をしなければなりません。

これは、悪意を持ったユーザーに通信内容を見られても中身がわからないようにするためです。

AES は暗号化アルゴリズムのひとつで、共通鍵暗号方式が採用されています。
つまり、暗号化と復号に同じキーを使うことで、そのキーを知らない人には中身を知ることができない仕組みです。

C# でも Aes というクラスが用意されており、これを使うことで簡単に暗号化と復号ができます。
以下の公式ドキュメントを見ると実装サンプルが載っていますが、あまり実践的ではないと感じました。

https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.aes?view=net-7.0

実践的ではない理由としては以下のとおりです。

  • 共通鍵とするキー情報をプログラム内で生成して共有している
    • あらかじめ生成してクライアント/サーバー間で共有しておく必要がある
  • 復号時に使用する IV に同じ変数を使用している
    • 暗号化された中身の先頭から IV を取り出して復号するのが一般的である

本記事では、上記の問題を解決しながら実際のコードを書いていこうと思います。

AES を使用した暗号化と復号の手順

完成形のコードはこちらです。
https://github.com/nekojoker/AesSample

事前準備

暗号化と復号で共通に使用するキーはバイト配列です。
しかしバイト配列のまま値を持っておくのは難しいので、base64 形式で持っておき、必要に応じてバイト配列に変換するようにしたいと思います。

コンソールアプリで以下のコードを実行すれば、base64 形式のキーを得ることができます。
今回は、StrKey という定数で定義しておきました。

using (var aes = Aes.Create())
{
    Console.WriteLine(Convert.ToBase64String(aes.Key));
}

// 得られる値の例
// 9ojffFV/YtBvZZJ48ytVv5zkPyJIb0m0teXE8M3xd0U=

暗号化

暗号化のコードがこちらです。

private static async ValueTask<string> EncryptAsync(string plainText, CancellationToken cancellationToken = default)
{
    var key = Convert.FromBase64String(StrKey);

    using (var aes = Aes.Create())
    {
        var encryptor = aes.CreateEncryptor(key, aes.IV);
        using (var ms = new MemoryStream())
        await using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
        {
            await ms.WriteAsync(aes.IV, cancellationToken);
            await using (var sw = new StreamWriter(cs))
            {
                await sw.WriteAsync(plainText);
            }
            var bytes = ms.ToArray();
            return Convert.ToBase64String(bytes);
        }
    }
}

冒頭で少し書いた通り、暗号化するとき先頭に IV の情報を付与して、復号時に取り出すのが一般的です。
ms.WriteAsync にて IV を先頭に付与しています。
平文の情報は、sw.WriteAsync にて書き込んでいます。

返り値は byte[] でも問題ありませんが、クライアント・サーバー間の通信の都合上、文字列のほうが扱いやすかったりするので、今回は base64 の返り値としました。

復号

復号のコードがこちらです。

private async ValueTask<string> DecryptAsync(byte[] cipherText, CancellationToken cancellationToken = default)
{
    var iv = new byte[16];
    Array.Copy(cipherText, 0, iv, 0, iv.Length);
    using (var aes = Aes.Create())
    {
        var decryptor = aes.CreateDecryptor(this.Key, iv);
        using (var ms = new MemoryStream())
        {
            await using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
            await using (var bw = new BinaryWriter(cs))
            {
                bw.Write(cipherText, iv.Length, cipherText.Length - iv.Length);
            }
            var bytes = ms.ToArray();
            return System.Text.Encoding.Default.GetString(bytes);
        }
    }
}

暗号化するときに追加した IV を取り出し、復号で使用します。
また、そのまま復号すると IV まで一緒に復号されてしまうので、IV の後ろから複合するようにしています。
最後に、バイト配列で復号した値を文字列として取り出せば、元の値が取得できます。

動作確認

コンソールアプリを作成して、以下のコードで動かしてみます。

static async Task Main(string[] args)
{
    var plainText = "Hello World!!";
    Console.WriteLine($"PlainText: {plainText}");

    var encrypt = await EncryptAsync(plainText);
    Console.WriteLine($"Encrypt: {encrypt}");

    var cipherText = Convert.FromBase64String(encrypt);
    var decrypt = await DecryptAsync(cipherText);
    Console.WriteLine($"Decrypt: {decrypt}");
}

暗号化した値を元通り復号することができました。

PlainText: Hello World!!
Encrypt: AfDBcxQ/g3dormFJV1orSaL+Qyan9xYTnmp4RiLiHRU=
Decrypt: Hello World!!

パスワードなどは暗号化して、安全にクライアントとサーバー間で通信をするようにしましょう。

参考文献

Discussion