C# で暗号用乱数生成・暗号化・ハッシュ化

# この記事の目的

C# を用いて、

* 暗号用乱数生成器によるIV (初期化ベクトル) の生成
* 暗号化・復号 (AES)
* ハッシュ化 (SHA-256)

を行い、一連の操作を行う実用的なサンプルを作成する。
平文にそのハッシュ値を連結したデータを暗号化し、復号時にハッシュ値を検証することで暗号文が有効かを確認できるようにする。

この記事では、それぞれのクラスのメソッドなどは今回のサンプルプログラムで使用したもののみを紹介している。
それぞれのクラスには、他のメソッド (同じ名前でとる引数が異なるメソッドを含む) も存在する。

# 暗号用乱数生成

[RandomNumberGenerator クラス](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.randomnumbergenerator?view=net-8.0)を用いることで、暗号用の乱数を生成できる。

まず、[Create() 静的メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.randomnumbergenerator.create?view=net-8.0#system-security-cryptography-randomnumbergenerator-create)により、このクラスのオブジェクトを生成する。

次に、生成したオブジェクトの [GetBytes() メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes?view=net-8.0#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()))を用いると、指定した `byte[]` 配列を乱数列で埋めることができる。

```csharp
RandomNumberGenerator rng = RandomNumberGenerator.Create();
byte[] randomBytes = new byte[16];
rng.GetBytes(randomBytes); // 配列をランダムデータで埋める
```

# 暗号化・復号

[Aes クラス](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.aes?view=net-8.0)を用いることで、バイト列の AES による暗号化や復号を行うことができる。

まず、[Create() 静的メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.aes.create?view=net-8.0#system-security-cryptography-aes-create)により、このクラスのオブジェクトを生成する。
デフォルトでは、CBC モードおよび PKCS7 パディングを用いる。

次に、生成したオブジェクトを用いて、暗号化時は [CreateEncryptor() メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.symmetricalgorithm.createencryptor?view=net-8.0#system-security-cryptography-symmetricalgorithm-createencryptor(system-byte()-system-byte()))、復号時は [CreateDecryptor() メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.symmetricalgorithm.createdecryptor?view=net-8.0#system-security-cryptography-symmetricalgorithm-createdecryptor(system-byte()-system-byte()))により [ICryptoTransform インターフェイス](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.icryptotransform?view=net-8.0)のオブジェクトを生成する。
これらのメソッドは、引数として鍵と IV (いずれも `byte[]`) をとる。

この `ICryptoTransform` オブジェクトの [TransformFinalBlock() メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.icryptotransform.transformfinalblock?view=net-8.0#system-security-cryptography-icryptotransform-transformfinalblock(system-byte()-system-int32-system-int32))にデータを渡すことで、データの暗号化や復号ができる。
このとき、データが格納された `byte[]` 配列に加え、暗号化や復号を行う最初の要素の添字と要素数を指定する。

```csharp
byte[] plainText = new byte[20]; // 平文：長さはブロックサイズの倍数じゃなくてもOK
byte[] key = new byte[16];       // 鍵：16バイト・24バイト・32バイトのいずれか
byte[] iv = new byte[16];        // IV：16バイト
Aes aes = Aes.Create();
// 暗号化を行う変換器を取得する
ICryptoTransform encryptor = aes.CreateEncryptor(key, iv);
// 配列 plainText 全体を暗号化する
byte[] cipherText = encryptor.TransformFinalBlock(plainText, 0, plainText.Length);
```

# ハッシュ化

[SHA256 クラス](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.sha256?view=net-8.0)を用いることで、バイト列の SHA-256 ハッシュ値を求めることができる。

まず、[Create() 静的メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.sha256.create?view=net-8.0#system-security-cryptography-sha256-create)により、このクラスのオブジェクトを生成する。

次に、生成したオブジェクトの [ComputeHash() メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.hashalgorithm.computehash?view=net-8.0#system-security-cryptography-hashalgorithm-computehash(system-byte()))を用いると、指定したバイト列 (`byte[]` 配列) の SHA-256 ハッシュ値を求め、`byte[]` 配列で得ることができる。
また、[HashSize プロパティ](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.hashalgorithm.hashsize?view=net-8.0#system-security-cryptography-hashalgorithm-hashsize)により、生成するハッシュ値の**ビット**数を求めることができる。

```csharp
SHA256 sha = SHA256.Create();
int size = sha.HashSize / 8; // SHA-256 ハッシュ値のバイト数
byte[] data = new byte[]{0xde, 0xad, 0xbe, 0xef}; // ハッシュ化するデータ
byte[] hash = sha.ComputeHash(data); // SHA-256 ハッシュ値を求める
```

# その他

今回のサンプルプログラムの作成にあたり、以下を参考にした。

* 配列の結合
  * [配列を連結するには［C#／VB］：.NET TIPS - ＠IT](https://atmarkit.itmedia.co.jp/ait/articles/1712/27/news018.html)
* 配列の内容の比較 (一致判定)
  * [【C#】知られざる比較の高速化戦略 #C# - Qiita](https://qiita.com/Kujiro/items/e687b089a0348d989f1b)

# サンプルプログラム

```csharp:EncryptTest.cs
using System;
using System.Linq;
using System.Security.Cryptography;

class EncryptTest
{
	private static Aes aes = Aes.Create();
	private static RandomNumberGenerator rng = RandomNumberGenerator.Create();
	private static SHA256 sha = SHA256.Create();

	// バイト配列 a と b を結合した配列を返す
	private static byte[] ConcatArray(byte[] a, byte[] b)
	{
		byte[] res = new byte[a.Length + b.Length];
		a.CopyTo(res, 0);
		b.CopyTo(res, a.Length);
		return res;
	}

	// 平文 data を鍵 key で暗号化した結果を返す
	public static byte[] Encrypt(byte[] data, byte[] key)
	{
		// IVを生成する
		byte[] iv = new byte[16];
		rng.GetBytes(iv);

		// SHA-256ハッシュ値を求める
		byte[] hash = sha.ComputeHash(data);

		// データとハッシュ値を暗号化する
		ICryptoTransform encryptor = aes.CreateEncryptor(key, iv);
		byte[] plainText = ConcatArray(data, hash);
		byte[] cipherText = encryptor.TransformFinalBlock(plainText, 0, plainText.Length);

		// IVと暗号文を連結して返す
		byte[] result = ConcatArray(iv, cipherText);
		return result;
	}

	// 暗号文 data を鍵 key で復号した結果を返す
	public static byte[] Decrypt(byte[] data, byte[] key)
	{
		// IVを取り出す
		byte[] iv = new byte[16];
		Array.Copy(data, 0, iv, 0, iv.Length);

		// データとハッシュ値を復号する
		ICryptoTransform decryptor = aes.CreateDecryptor(key, iv);
		byte[] plainText = decryptor.TransformFinalBlock(data, iv.Length, data.Length - iv.Length);

		// データとハッシュ値を分離する
		byte[] decryptedData = new byte[plainText.Length - sha.HashSize / 8];
		byte[] decryptedHash = new byte[plainText.Length - decryptedData.Length];
		Array.Copy(plainText, 0, decryptedData, 0, decryptedData.Length);
		Array.Copy(plainText, decryptedData.Length, decryptedHash, 0, decryptedHash.Length);

		// データのSHA-256ハッシュ値を求める
		byte[] hash = sha.ComputeHash(decryptedData);

		// 求めたハッシュ値を復号したハッシュ値と比較する
		if (!hash.SequenceEqual(decryptedHash))
		{
			throw new Exception("hash mismatch");
		}

		// 復号したデータを返す
		return decryptedData;
	}
}
```

```csharp:EncryptTestUser.cs
using System;
using System.Text;
using System.Text.RegularExpressions;

class EncryptTestUser
{
	// バイト列を、16バイトごとに改行を入れた文字列に変換する
	private static string BytesToString(byte[] data)
	{
		string rawResult = BitConverter.ToString(data);
		Regex regex = new Regex("(([0-9a-fA-F]{2}-){15}[0-9a-fA-F]{2})-");
		return regex.Replace(rawResult, "$1\n");
	}

	public static void Main(string[] args)
	{
		byte[] key = new byte[] {
			0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
			0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
			0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
			0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
		};
		Console.WriteLine("key:");
		Console.WriteLine(BytesToString(key));

		string plainText = "hello, world";
		Console.WriteLine("\nplain text:");
		Console.WriteLine(plainText);

		byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
		Console.WriteLine("\nplain text (bytes):");
		Console.WriteLine(BytesToString(plainTextBytes));

		byte[] cipherText = EncryptTest.Encrypt(plainTextBytes, key);
		Console.WriteLine("\ncipher text (bytes):");
		Console.WriteLine(BytesToString(cipherText));

		byte[] decryptedTextBytes = EncryptTest.Decrypt(cipherText, key);
		Console.WriteLine("\ndecrypted text (bytes):");
		Console.WriteLine(BytesToString(decryptedTextBytes));

		string decryptedText = Encoding.UTF8.GetString(decryptedTextBytes);
		Console.WriteLine("\ndecrypted text:");
		Console.WriteLine(decryptedText);
	}
}
```

```text:出力例
key:
01-23-45-67-89-AB-CD-EF-00-11-22-33-44-55-66-77
88-99-AA-BB-CC-DD-EE-FF-FE-DC-BA-98-76-54-32-10

plain text:
hello, world

plain text (bytes):
68-65-6C-6C-6F-2C-20-77-6F-72-6C-64

cipher text (bytes):
A1-2E-9C-D5-9D-02-BC-A5-A7-43-EC-72-86-3F-49-3C
D5-8E-04-CB-6F-8B-4E-F8-45-7E-89-AC-A0-C6-DC-9F
38-10-BF-C9-74-94-80-3D-F3-30-87-7D-65-57-30-97
04-BB-75-E5-B5-39-EB-12-AD-D9-78-44-C1-D7-DF-B8

decrypted text (bytes):
68-65-6C-6C-6F-2C-20-77-6F-72-6C-64

decrypted text:
hello, world
```

# まとめ

C# で暗号用乱数生成・暗号化 (AES)・ハッシュ化 (SHA-256) を行う方法を確認した。
さらに、これらを用いて実際に暗号化や復号を行うサンプルを作成した。