【C#】AES を使用した暗号化と復号の手順
ログインパスワードなど、そのままの状態でネットワーク上に流してはいけないものについては、暗号化して安全にクライアントとサーバー間で通信をしなければなりません。
これは、悪意を持ったユーザーに通信内容を見られても中身がわからないようにするためです。
AES は暗号化アルゴリズムのひとつで、共通鍵暗号方式が採用されています。
つまり、暗号化と復号に同じキーを使うことで、そのキーを知らない人には中身を知ることができない仕組みです。
C# でも Aes というクラスが用意されており、これを使うことで簡単に暗号化と復号ができます。
以下の公式ドキュメントを見ると実装サンプルが載っていますが、あまり実践的ではないと感じました。
実践的ではない理由としては以下のとおりです。
- 共通鍵とするキー情報をプログラム内で生成して共有している
- あらかじめ生成してクライアント/サーバー間で共有しておく必要がある
- 復号時に使用する IV に同じ変数を使用している
- 暗号化された中身の先頭から IV を取り出して復号するのが一般的である
本記事では、上記の問題を解決しながら実際のコードを書いていこうと思います。
AES を使用した暗号化と復号の手順
完成形のコードはこちらです。
事前準備
暗号化と復号で共通に使用するキーはバイト配列です。
しかしバイト配列のまま値を持っておくのは難しいので、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