【C#】AES暗号化・復号化の実装手順と鍵管理のポイントをわかりやすく解説
C#でAES暗号化を使うと、対称鍵方式の強力なセキュリティを簡潔に実装できるため、通信やファイル保存の機密性を高められます。
Aes.Create()
で鍵とIVを生成し、CreateEncryptor
とCreateDecryptor
を用いてバイト配列を安全に変換するだけで完結します。
鍵とIVを安全に管理すれば、速度と安全性のバランスに優れた暗号化環境が得られます。
AES暗号化の基本
AES(Advanced Encryption Standard)は、現代の情報セキュリティにおいて広く使われている対称鍵暗号方式の一つです。
ここでは、AES暗号化の基本的な仕組みや特徴についてわかりやすく解説します。
対称鍵暗号が選ばれる理由
暗号方式には大きく分けて「対称鍵暗号」と「公開鍵暗号」の2種類があります。
AESは対称鍵暗号に分類され、暗号化と復号化に同じ鍵を使います。
対称鍵暗号が選ばれる主な理由は以下の通りです。
- 高速な処理性能
対称鍵暗号は計算コストが低く、暗号化・復号化の処理が高速です。
大量のデータを扱う場合やリアルタイム通信での暗号化に適しています。
- シンプルな鍵管理
同じ鍵を使うため、鍵の管理が比較的シンプルです。
ただし、鍵の安全な共有が課題となります。
- 広範な用途
ファイル暗号化、通信の保護、データベースの暗号化など、さまざまなシナリオで利用されています。
一方で、鍵の共有や配布に課題があるため、公開鍵暗号と組み合わせて使うことも多いです。
例えば、公開鍵暗号でAESの鍵を安全に共有し、その後はAESで高速にデータを暗号化するハイブリッド方式が一般的です。
ブロック暗号モード(CBC・GCMなど)の仕組み
AESはブロック暗号であり、固定長のデータブロック(通常128ビット)単位で暗号化を行います。
しかし、実際のデータは任意の長さであるため、ブロック暗号モード(Block Cipher Mode)を使って複数のブロックを連結して処理します。
代表的なモードには以下があります。
モード名 | 特徴 | 利用例 |
---|---|---|
CBC(Cipher Block Chaining) | 各ブロックの暗号化に前の暗号文ブロックを利用し、連鎖的に処理 | ファイル暗号化、TLS初期 |
GCM(Galois/Counter Mode) | カウンタモードに認証機能を加えたモード。暗号化と同時に改ざん検知が可能 | TLS 1.2以降、モバイル通信 |
CBCモードの仕組み
CBCモードでは、最初のブロックを暗号化する前に初期化ベクトル(IV)と平文ブロックをXOR演算します。
次のブロックは前の暗号文ブロックとXORしてから暗号化します。
これにより、同じ平文でも異なるIVを使えば異なる暗号文が生成され、暗号文のパターンが隠されます。
ただし、CBCは改ざん検知機能を持たないため、別途MAC(Message Authentication Code)などで認証を行う必要があります。
GCMモードの仕組み
GCMはカウンタモード(CTR)をベースにしており、暗号化と同時に認証タグを生成します。
これにより、データの改ざんを検知できるため、セキュリティが向上します。
さらに、並列処理が可能で高速です。
GCMモードはTLSやIPsecなどの通信プロトコルで広く採用されており、最新のセキュリティ要件に適合しています。
パディング方式(PKCS#7ほか)の役割
AESは128ビット(16バイト)単位のブロック暗号であるため、暗号化するデータの長さが16バイトの倍数でなければなりません。
しかし、実際のデータは任意の長さであるため、最後のブロックを埋めるために「パディング」と呼ばれる余分なデータを付加します。
代表的なパディング方式には以下があります。
パディング方式 | 説明 | 特徴 |
---|---|---|
PKCS#7 | 不足分のバイト数をその数値で埋める | 最も一般的で広く使われている |
Zero Padding | 不足分を0x00で埋める | テキストデータ以外で使われることが多い |
ANSI X.923 | 不足分を0x00で埋め、最後のバイトにパディング長を記録 | 一部のシステムで利用 |
PKCS#7パディングの具体例
例えば、暗号化するデータが「HELLO」(5バイト)であった場合、16バイトに満たないため11バイトのパディングが必要です。
PKCS#7では、11を表す0x0Bを11回繰り返して埋めます。
データ | H | E | L | L | O | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B | 0x0B |
---|
復号化時には、このパディングを検出して取り除くことで元のデータを正しく復元できます。
パディングの重要性
パディングが正しく行われないと、復号化時にエラーが発生したり、データが破損したりします。
また、パディングの不適切な処理は「パディングオラクル攻撃」と呼ばれるセキュリティリスクを招くこともあります。
そのため、パディング方式は標準的なものを使い、復号時には例外処理を適切に行うことが重要です。
AESアルゴリズムと鍵長
128bit・192bit・256bit鍵の比較
AESは鍵長によって3つのバリエーションがあり、それぞれ128ビット、192ビット、256ビットの鍵長を持ちます。
鍵長が長くなるほど理論上の安全性は高まりますが、処理速度やリソース消費にも影響します。
鍵長 | 鍵のバイト数 | ラウンド数 | セキュリティレベル | 処理速度の目安 |
---|---|---|---|---|
128bit | 16バイト | 10 | 十分な安全性。現在の標準的な用途に最適 | 最速 |
192bit | 24バイト | 12 | 128bitより強固だが、利用例は限定的 | 中程度 |
256bit | 32バイト | 14 | 最も強力。将来の耐量子計算機対策にも期待 | 最も遅い |
- 128bit鍵
AES-128は現在の多くのシステムで標準的に使われています。
十分な安全性を持ち、暗号化・復号化の処理も高速です。
多くのハードウェアやソフトウェアで最適化されているため、パフォーマンス面で優れています。
- 192bit鍵
AES-192はAES-128とAES-256の中間に位置しますが、実際の利用はあまり多くありません。
セキュリティ強度はAES-128より高いものの、AES-256の方がより強力であるため、AES-192はあまり選択されない傾向にあります。
- 256bit鍵
AES-256は最も強力な鍵長であり、特に高いセキュリティが求められる環境で使われます。
量子コンピュータの脅威に対しても耐性があると考えられており、政府機関や金融機関などで採用例が多いです。
ただし、処理速度はAES-128に比べて遅くなるため、パフォーマンス要件とのバランスを考慮する必要があります。
セキュリティ要件に合わせた鍵長選択
鍵長の選択は、システムのセキュリティ要件やパフォーマンス要件に応じて決めることが重要です。
以下のポイントを参考にしてください。
- 一般的な用途やパフォーマンス重視の場合
AES-128が推奨されます。
現在の標準的なセキュリティレベルを満たしつつ、高速な処理が可能です。
多くのアプリケーションや通信プロトコルで採用されています。
- 高いセキュリティが必要な場合
AES-256を選択します。
特に機密性の高いデータや長期間の保護が求められる場合に適しています。
将来的な量子コンピュータの影響を考慮する場合もAES-256が望ましいです。
- 中間的な選択肢としてのAES-192
特別な理由がない限り、AES-192はあまり使われません。
AES-128かAES-256のどちらかを選ぶのが一般的です。
- 法規制や標準準拠の要件
一部の業界や国ではAES-256の使用が義務付けられている場合があります。
例えば、米国政府の機密情報保護にはAES-256が推奨されています。
こうした規制に準拠する必要がある場合は、鍵長の選択に注意してください。
- パフォーマンスとセキュリティのバランス
鍵長が長くなるほど暗号化・復号化の処理に時間がかかります。
リアルタイム性が求められるシステムではAES-128が適していることが多いです。
一方で、バッチ処理や保存データの暗号化など、処理速度よりも安全性を優先する場合はAES-256を選ぶとよいでしょう。
これらのポイントを踏まえ、システムの要件に最適な鍵長を選択することが安全かつ効率的なAES暗号化の実装につながります。
.NET標準ライブラリで使うAES
AesとAesManagedの違い
.NETでAES暗号化を行う際に使われるクラスとして、Aes
とAesManaged
があります。
両者は似ていますが、いくつか重要な違いがあります。
Aes
クラス
Aes
は抽象クラスであり、Aes.Create()
メソッドを使って環境に最適なAES実装を取得します。
Windows環境では通常、OSのネイティブ暗号API(CNG: Cryptography Next Generation)を利用するため、高速かつセキュアな処理が可能です。
クロスプラットフォーム対応も進んでおり、.NET Coreや.NET 5以降で推奨されています。
AesManaged
クラス
AesManaged
は完全にマネージドコードで実装されたAES暗号化クラスです。
純粋なC#で書かれているため、プラットフォームに依存しませんが、パフォーマンスはネイティブ実装に劣ります。
また、.NET Framework 4.6以降は非推奨となっており、新規開発ではAes
クラスの使用が推奨されています。
特徴 | Aes | AesManaged |
---|---|---|
実装 | ネイティブAPI利用(環境依存) | 完全マネージドコード |
パフォーマンス | 高速 | やや遅い |
クロスプラットフォーム | あり(.NET Core/.NET 5以降) | あり |
推奨度 | 推奨 | 非推奨(レガシー向け) |
実際の開発では、Aes.Create()
を使ってAes
インスタンスを取得し、環境に最適な実装を利用するのがベストプラクティスです。
CreateEncryptor/CreateDecryptorの流れ
AES暗号化・復号化の処理は、Aes
クラスのCreateEncryptor
メソッドとCreateDecryptor
メソッドを使って行います。
これらはICryptoTransform
インターフェースを返し、暗号化・復号化の変換処理を担当します。
CreateEncryptor(byte[] key, byte[] iv)
指定した鍵(key)
と初期化ベクトル(iv)
を使って暗号化用の変換器を作成します。
返されるICryptoTransform
は、平文データを暗号文に変換するために使います。
CreateDecryptor(byte[] key, byte[] iv)
同様に、復号化用の変換器を作成します。
暗号文を平文に戻す処理を行います。
この流れは以下のようになります。
Aes.Create()
でAESインスタンスを生成。- 鍵とIVを設定。
CreateEncryptor
またはCreateDecryptor
で変換器を取得。- 変換器を使ってデータを変換。
この変換器はストリームやバイト配列に対して適用でき、CryptoStream
と組み合わせて使うことが多いです。
CryptoStreamでストリーム暗号化
CryptoStream
は、ストリームに対して暗号化や復号化を透過的に行うためのクラスです。
Stream
をラップし、読み書きの際に自動的に暗号変換を適用します。
基本的な使い方
- 暗号化時
書き込み用のCryptoStream
を作成し、平文データを書き込むと自動的に暗号化されて下流のストリームに書き込まれます。
- 復号化時
読み込み用のCryptoStream
を作成し、暗号文を読み込むと自動的に復号化されて平文が得られます。
具体例の流れ
- 暗号化の場合、
MemoryStream
やFileStream
などの出力先ストリームを用意。 CreateEncryptor
で取得したICryptoTransform
を使い、CryptoStream
を作成CryptoStreamMode.Write
。StreamWriter
などで平文を書き込むと、暗号化されたデータがストリームに流れます。- 復号化の場合は逆に、暗号文を読み込むストリームを用意し、
CreateDecryptor
のICryptoTransform
でCryptoStream
を作成CryptoStreamMode.Read
。 StreamReader
などで読み込むと復号化された平文が得られます。
メリット
- 大きなデータやファイルをチャンク単位で処理できるため、メモリ効率が良いでしょう
- ストリームの読み書きに自然に組み込めるため、コードがシンプルになります
- 暗号化・復号化の処理を意識せずにストリーム操作が可能です
注意点
CryptoStream
の書き込みや読み込みが完了したら、必ずFlushFinalBlock()
やDispose()
で処理を終了させる必要があります。これによりパディングの処理や最終ブロックの書き込みが正しく行われます
以上のように、.NET標準ライブラリのAes
クラスとCryptoStream
を組み合わせることで、安全かつ効率的にAES暗号化・復号化を実装できます。
鍵とIVの生成・保管
RandomNumberGeneratorによる安全な乱数生成
AES暗号化において、鍵(Key)と初期化ベクトル(IV)は非常に重要な役割を果たします。
これらは予測不可能でなければならず、安全な乱数生成が必須です。
C#では、System.Security.Cryptography.RandomNumberGenerator
クラスを使って安全な乱数を生成できます。
RandomNumberGenerator
は暗号学的に強力な乱数を生成するため、単純なRandom
クラスとは異なり、攻撃者に推測されにくい乱数を作り出します。
鍵やIVの生成に必ず使うべきクラスです。
以下は、16バイト(128ビット)の安全な乱数を生成する例です。
using System.Security.Cryptography;
byte[] key = new byte[16]; // 128bit鍵
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(key);
}
この方法で生成したバイト配列は、AESの鍵やIVとしてそのまま利用できます。
IVは通常16バイト(128ビット)で、鍵長に関わらず固定長です。
鍵・IVの永続化とセキュアストレージ
生成した鍵やIVは、暗号化と復号化の両方で同じものを使う必要があるため、適切に保存・管理しなければなりません。
以下のポイントに注意してください。
- 鍵の保管は厳重に
鍵が漏洩すると暗号化の意味がなくなるため、平文での保存は避けます。
可能な限りハードウェアセキュリティモジュール(HSM)やOSのセキュアストレージ(WindowsのDPAPI、Azure Key Vault、macOSのKeychainなど)を利用します。
- IVの保管
IVは秘密にする必要はありませんが、暗号化時にランダムに生成し、復号時に同じIVを使うために保存または暗号文に付加しておく必要があります。
多くの場合、暗号文の先頭にIVを付けて一緒に保存します。
- ファイルやデータベースへの保存例
鍵は暗号化して保存するか、セキュアストレージに格納します。
IVは暗号文とセットで保存し、復号時に分離して使います。
- 環境変数や設定ファイルの利用は注意
鍵を環境変数や設定ファイルに平文で置くのは避けるべきです。
どうしても必要な場合は、暗号化やアクセス制御を強化してください。
PBKDF2での鍵派生
ユーザーが覚えやすいパスワードや文字列からAESの鍵を生成する場合、直接パスワードを鍵として使うのは危険です。
パスワードは推測されやすく、長さや形式も不定のため、鍵としての安全性が不足します。
そこで、PBKDF2(Password-Based Key Derivation Function 2)を使ってパスワードから安全な鍵を派生させます。
PBKDF2は以下の特徴を持ちます。
- ソルト(Salt)を利用
ランダムなソルトを加えることで、同じパスワードでも異なる鍵を生成し、レインボーテーブル攻撃を防ぎます。
- 反復回数(Iteration Count)
計算を複数回繰り返すことで、総当たり攻撃のコストを増やします。
推奨される反復回数は環境により異なりますが、数万回以上が一般的です。
- 出力長の指定
AESの鍵長に合わせて、128bit、192bit、256bitの鍵を生成可能です。
以下はPBKDF2を使ってパスワードから256bitのAES鍵を生成する例です。
using System.Security.Cryptography;
using System.Text;
string password = "ユーザーのパスワード";
byte[] salt = new byte[16];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
int iterations = 100_000; // 反復回数
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
byte[] key = pbkdf2.GetBytes(32); // 256bit鍵
}
この方法で生成したkey
は安全なAES鍵として利用できます。
ソルトは鍵と一緒に保存し、復号時に同じパスワードとソルトで鍵を再生成します。
PBKDF2を使うことで、パスワードの弱さを補い、安全な鍵を得られるため、パスワードベースの暗号化には必須の技術です。
暗号化処理の実装フロー
文字列からバイト配列への変換
AES暗号化はバイト配列単位で処理を行うため、まずは暗号化したい文字列をバイト配列に変換する必要があります。
C#ではSystem.Text.Encoding
クラスを使って簡単に変換できます。
一般的にはUTF-8エンコーディングを使うことが多く、以下のように変換します。
using System.Text;
string plainText = "これは秘密のメッセージです。";
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
このplainBytes
をAESの暗号化処理に渡すことで、文字列を安全に暗号化できます。
逆に復号化後は、バイト配列から文字列に戻すためにEncoding.UTF8.GetString
を使います。
文字列をバイト配列に変換する際のポイントは以下の通りです。
- エンコーディングの統一
暗号化・復号化の両方で同じエンコーディングを使うことが重要です。
異なるエンコーディングを使うと文字化けやデータ破損の原因になります。
- バイナリデータの扱い
文字列以外のバイナリデータを暗号化する場合は、変換は不要でそのままバイト配列を使います。
ストリームAPIでの暗号化書き込み
.NETのCryptoStream
を使うと、ストリームに対して暗号化処理を透過的に行えます。
これにより、ファイルやメモリ上のデータを効率的に暗号化できます。
暗号化の基本的な流れは以下の通りです。
- 暗号化対象の出力先ストリーム(例:
MemoryStream
やFileStream
)を用意。 Aes.Create()
でAESインスタンスを生成し、鍵とIVを設定。CreateEncryptor
で暗号化用のICryptoTransform
を取得。CryptoStream
を作成し、出力先ストリームと暗号化変換器を紐付けます。StreamWriter
やBinaryWriter
で平文データを書き込むと、自動的に暗号化されてストリームに流れる。
以下は文字列をメモリ上で暗号化する例です。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
string plainText = "これは秘密のメッセージです。";
byte[] key, iv;
// 鍵とIVの生成(例として固定値ではなくランダム生成)
using (Aes aes = Aes.Create())
{
aes.GenerateKey();
aes.GenerateIV();
key = aes.Key;
iv = aes.IV;
}
byte[] encryptedBytes;
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
{
swEncrypt.Write(plainText);
}
encryptedBytes = msEncrypt.ToArray();
}
}
}
Console.WriteLine($"暗号化データ(Base64): {Convert.ToBase64String(encryptedBytes)}");
暗号化データ(Base64): 例: 3q2+7w==(実際の出力は毎回異なります)
この例では、StreamWriter
を使って文字列を書き込むと、CryptoStream
が自動的に暗号化し、MemoryStream
に暗号文が蓄積されます。
最後にToArray()
で暗号化されたバイト配列を取得しています。
連続データのチャンク暗号化
大きなデータやストリームを一度にメモリに読み込むのは非効率であり、メモリ不足の原因にもなります。
そこで、チャンク(小分割)単位でデータを分割し、順次暗号化する方法が有効です。
CryptoStream
はストリームベースの処理なので、チャンク単位の読み書きに適しています。
以下のポイントを押さえて実装します。
- チャンクサイズの設定
例えば4KBや8KBなど適切なバッファサイズを決めて、入力ストリームから順次読み込みます。
- 逐次書き込み
読み込んだチャンクをCryptoStream
に書き込み、暗号化されたデータを出力ストリームに流します。
- 最後のチャンクのパディング処理
CryptoStream
は内部でパディングを自動処理するため、最後の書き込み後にFlushFinalBlock()
を呼び出して処理を完了させます。
以下はファイルをチャンク単位で暗号化する例です。
using System;
using System.IO;
using System.Security.Cryptography;
string inputFile = "input.txt";
string outputFile = "encrypted.bin";
byte[] key, iv;
using (Aes aes = Aes.Create())
{
aes.GenerateKey();
aes.GenerateIV();
key = aes.Key;
iv = aes.IV;
}
using (FileStream fsInput = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
using (FileStream fsOutput = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (CryptoStream csEncrypt = new CryptoStream(fsOutput, encryptor, CryptoStreamMode.Write))
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fsInput.Read(buffer, 0, buffer.Length)) > 0)
{
csEncrypt.Write(buffer, 0, bytesRead);
}
csEncrypt.FlushFinalBlock();
}
}
Console.WriteLine("ファイルの暗号化が完了しました。");
この例では、input.txt
を4KBずつ読み込み、CryptoStream
を通じて暗号化しながらencrypted.bin
に書き込んでいます。
FlushFinalBlock()
でパディング処理を完了させることが重要です。
チャンク暗号化は大容量ファイルやネットワークストリームの暗号化に適しており、メモリ使用量を抑えつつ安全に処理できます。
復号化処理の実装フロー
バイト配列から平文文字列への復元
AESで暗号化されたデータはバイト配列として扱われます。
復号化処理では、この暗号化されたバイト配列を元の平文文字列に戻す必要があります。
復号化の基本的な流れは以下の通りです。
- AESインスタンスを生成し、暗号化時と同じ鍵(Key)と初期化ベクトル(IV)を設定します。
CreateDecryptor
メソッドで復号化用のICryptoTransform
を取得します。MemoryStream
に暗号化されたバイト配列をセットし、CryptoStream
を使って復号化ストリームを作成します。StreamReader
で復号化ストリームから読み込み、元の文字列を取得します。
以下は具体的なコード例です。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
byte[] encryptedBytes = /* 暗号化済みのバイト配列 */;
byte[] key = /* 暗号化時の鍵 */;
byte[] iv = /* 暗号化時のIV */;
string decryptedText;
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream msDecrypt = new MemoryStream(encryptedBytes))
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
using (StreamReader srDecrypt = new StreamReader(csDecrypt, Encoding.UTF8))
{
decryptedText = srDecrypt.ReadToEnd();
}
}
Console.WriteLine($"復号化されたテキスト: {decryptedText}");
この例では、StreamReader
が復号化されたバイトストリームを文字列として読み込み、元の平文を復元しています。
暗号化時と同じエンコーディング(ここではUTF-8)を使うことが重要です。
CryptographicExceptionの捕捉と対策
復号化処理中にCryptographicException
が発生することがあります。
これは主に以下の原因によるものです。
- 鍵やIVが間違っている
暗号化時と異なる鍵やIVを使うと復号化に失敗します。
- 暗号文が破損または改ざんされている
データの一部が欠損したり改ざんされていると、復号化時にエラーが発生します。
- パディングエラー
パディング方式の不一致や不正なパディングがある場合に起こります。
これらの例外は適切に捕捉し、ユーザーにわかりやすいエラーメッセージを表示したり、ログに記録したりすることが重要です。
以下は例外処理の例です。
try
{
// 復号化処理(上記のコード例)
}
catch (CryptographicException ex)
{
Console.WriteLine("復号化に失敗しました。鍵やデータが正しいか確認してください。");
// ログ出力や再試行処理などをここに記述
}
また、復号化前にデータの整合性を検証する仕組みを導入することで、例外発生を減らすことが可能です。
改ざん検出ハッシュの検証
AESの標準的なブロック暗号モード(例:CBC)自体には改ざん検知機能がありません。
そのため、暗号文が改ざんされていないかを検証するために、別途ハッシュやMAC(Message Authentication Code)を使うことが推奨されます。
代表的な方法は以下の通りです。
- HMAC(Hash-based Message Authentication Code)
鍵付きハッシュ関数を使い、暗号文に対してMACを生成・検証します。
改ざんがあればMAC検証に失敗します。
- 認証付き暗号モード(例:GCM)
AES-GCMのように暗号化と同時に認証タグを生成し、復号時に改ざん検知を行います。
HMACによる改ざん検知の例
- 暗号化後に暗号文と一緒にHMACを生成し保存します。
- 復号化前に暗号文のHMACを再計算し、保存されたHMACと比較します。
- 一致しなければ改ざんと判断し、復号化を中止します。
using System.Security.Cryptography;
byte[] keyForHmac = /* HMAC用の鍵 */;
byte[] cipherText = /* 暗号文 */;
byte[] receivedHmac = /* 保存されているHMAC */;
using (var hmac = new HMACSHA256(keyForHmac))
{
byte[] computedHmac = hmac.ComputeHash(cipherText);
bool isValid = CryptographicOperations.FixedTimeEquals(computedHmac, receivedHmac);
if (!isValid)
{
throw new CryptographicException("データが改ざんされています。");
}
}
FixedTimeEquals
はタイミング攻撃を防ぐための比較メソッドです。
改ざん検知を実装することで、復号化時の安全性が大幅に向上し、不正なデータによる例外発生やセキュリティリスクを低減できます。
AES暗号化を安全に運用するためには、必ず改ざん検知の仕組みを組み合わせることをおすすめします。
エンコードとフォーマット
Base64での文字列表現
AES暗号化の結果はバイト配列として得られますが、そのままではテキストとして扱いにくいため、文字列表現に変換する必要があります。
最も一般的な方法がBase64エンコードです。
Base64はバイナリデータを64種類の英数字と記号で表現するエンコード方式で、テキストデータとして安全に扱えます。
メールやJSON、XMLなどのテキストベースのフォーマットに暗号化データを埋め込む際に便利です。
C#でのBase64エンコードはConvert.ToBase64String
メソッドを使います。
逆にBase64文字列からバイト配列に戻すにはConvert.FromBase64String
を使います。
byte[] encryptedBytes = /* AESで暗号化したバイト配列 */;
string base64String = Convert.ToBase64String(encryptedBytes);
Console.WriteLine($"Base64エンコードされた暗号文: {base64String}");
// 復号化時にBase64文字列をバイト配列に戻す
byte[] decodedBytes = Convert.FromBase64String(base64String);
Base64の特徴は以下の通りです。
- 可読性が高い
英数字と一部記号のみで構成されるため、テキストファイルやログに埋め込みやすいです。
- サイズが約33%増加
元のバイト数に対して約1.33倍の長さになるため、通信や保存容量に注意が必要です。
- 改行や空白に注意
Base64文字列は改行や空白を含めないように扱うのが一般的です。
改行が入ると復号時にエラーになることがあります。
Base64は暗号化データのテキスト化において最も広く使われている方法であり、AES暗号化の結果を安全かつ扱いやすくするために必須の技術です。
JSON・XMLへの埋め込み時の注意点
暗号化データをJSONやXMLなどの構造化テキストに埋め込む場合、いくつか注意すべきポイントがあります。
エンコード済みデータの利用
暗号化データはバイナリのままではJSONやXMLに直接埋め込めません。
必ずBase64などのテキストエンコードを行い、文字列として格納します。
これにより、パースエラーや文字化けを防げます。
特殊文字のエスケープ
JSONやXMLでは特定の文字(例:"
, <
, >
, &
)が特別な意味を持つため、これらが含まれる場合はエスケープが必要です。
ただし、Base64はこれらの特殊文字を含まないため、通常は追加のエスケープ処理は不要です。
改行コードの扱い
Base64エンコードされた文字列に改行が含まれる場合、JSONやXMLのパース時に問題が発生することがあります。
Base64エンコード時に改行を入れない設定を使うか、改行を除去してから埋め込みます。
文字コードの統一
JSONやXMLファイル全体の文字コード(通常UTF-8)と暗号化データのエンコードが一致していることを確認します。
異なると文字化けやパースエラーの原因になります。
フィールド名や属性名の命名
暗号化データを格納するフィールド名や属性名はわかりやすく、かつ他のデータと衝突しないように命名します。
例えば、"EncryptedData"
や"CipherText"
などが一般的です。
JSONの例
{
"UserId": 12345,
"EncryptedData": "3q2+7w=="
}
XMLの例
<User>
<UserId>12345</UserId>
<EncryptedData>3q2+7w==</EncryptedData>
</User>
サイズとパフォーマンスの考慮
Base64エンコードによりデータサイズが増加するため、大量の暗号化データをJSONやXMLに埋め込む場合は通信帯域やストレージ容量に注意が必要です。
必要に応じて圧縮や分割を検討してください。
これらのポイントを押さえることで、AES暗号化データをJSONやXMLに安全かつ正しく埋め込み、システム間でのデータ交換や保存をスムーズに行えます。
暗号モード別実装例
CBCモード
CBC(Cipher Block Chaining)モードはAESの代表的なブロック暗号モードの一つで、多くの既存システムで採用されています。
各ブロックの暗号化に前の暗号文ブロックを利用することで、同じ平文でも異なる暗号文を生成し、セキュリティを高めています。
パディング設定
CBCモードはブロック単位(通常16バイト)で処理するため、入力データの長さがブロックサイズの倍数でない場合、パディングが必要です。
C#のAes
クラスでは、Padding
プロパティでパディング方式を指定できます。
最も一般的なパディング方式はPaddingMode.PKCS7
です。
これは不足分のバイト数をその数値で埋める方式で、復号時に正確にパディングを取り除けます。
using (Aes aes = Aes.Create())
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
// 鍵とIVの設定など
}
パディングを正しく設定しないと、復号時にCryptographicException
が発生することがあります。
特に、暗号化と復号化でパディング方式が一致していることを必ず確認してください。
IV取り扱いの要点
CBCモードでは初期化ベクトル(IV)が重要な役割を果たします。
IVは最初のブロックの暗号化に使われ、同じ鍵でも異なるIVを使うことで暗号文の多様性を確保します。
- IVはランダムに生成する
毎回異なるランダムなIVを生成し、再利用を避けます。
Aes.GenerateIV()
で安全に生成可能です。
- IVは秘密にする必要はないが、復号時に必要
IVは暗号文と一緒に保存・送信します。
一般的には暗号文の先頭にIVを付加して一体化する方法が多いです。
- IVの長さはブロックサイズと同じ
AESのブロックサイズは128ビット(16バイト)なので、IVも16バイト固定です。
以下はIVを暗号文の先頭に付加して保存する例です。
byte[] iv = aes.IV;
byte[] encryptedData = /* 暗号化データ */;
byte[] combined = new byte[iv.Length + encryptedData.Length];
Buffer.BlockCopy(iv, 0, combined, 0, iv.Length);
Buffer.BlockCopy(encryptedData, 0, combined, iv.Length, encryptedData.Length);
復号時は先頭16バイトをIVとして分離し、残りを暗号文として復号処理に渡します。
GCMモード(.NET 5以降)
GCM(Galois/Counter Mode)は認証付き暗号モードで、暗号化と同時にデータの改ざん検知が可能です。
改ざん検知用の認証タグを生成し、復号時に検証します。
GCMは高速かつ安全で、TLS 1.2以降やモバイル通信で広く使われています。
.NET 5以降のAesGcm
クラスで利用可能です。
タグ長と認証手順
GCMモードでは、暗号化時に認証タグ(Authentication Tag)を生成します。
タグは通常16バイト(128ビット)ですが、長さは用途に応じて変更可能です。
- タグの役割
暗号文の改ざんを検知し、不正なデータの復号を防ぎます。
- 暗号化時の流れ
- 平文を暗号化しつつ、認証タグを生成。
- 暗号文とタグを保存または送信。
- 復号時の流れ
- 暗号文とタグを受け取ります。
- タグの検証に成功すれば復号化を行います。失敗すれば例外が発生し、改ざんを検知。
以下はAesGcm
を使った暗号化・復号化の例です。
using System;
using System.Security.Cryptography;
byte[] key = new byte[32]; // 256bit鍵
byte[] nonce = new byte[12]; // GCM推奨の12バイトのノンス
byte[] plaintext = System.Text.Encoding.UTF8.GetBytes("秘密のメッセージ");
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[16]; // 128bitタグ
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(key);
rng.GetBytes(nonce);
}
using (AesGcm aesGcm = new AesGcm(key))
{
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag);
}
// 復号化時
byte[] decrypted = new byte[plaintext.Length];
using (AesGcm aesGcm = new AesGcm(key))
{
aesGcm.Decrypt(nonce, ciphertext, tag, decrypted);
}
string decryptedText = System.Text.Encoding.UTF8.GetString(decrypted);
Console.WriteLine($"復号化されたテキスト: {decryptedText}");
タグの検証に失敗するとCryptographicException
が発生します。
AAD(追加認証データ)の活用
GCMモードは追加認証データ(AAD: Additional Authenticated Data)をサポートしています。
AADは暗号化はしませんが、認証タグの生成に含めることで、改ざん検知の対象にできます。
例えば、メッセージのヘッダー情報やメタデータをAADとして指定し、改ざんを防止できます。
byte[] aad = System.Text.Encoding.UTF8.GetBytes("ヘッダー情報");
using (AesGcm aesGcm = new AesGcm(key))
{
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag, aad);
}
using (AesGcm aesGcm = new AesGcm(key))
{
aesGcm.Decrypt(nonce, ciphertext, tag, decrypted, aad);
}
AADが復号時に異なると認証タグの検証に失敗し、復号は拒否されます。
これにより、暗号文だけでなく関連情報の整合性も保証できます。
CBCモードは互換性が高く広く使われていますが、改ざん検知機能がないため別途MACを組み合わせる必要があります。
一方、GCMモードは認証付き暗号として安全性が高く、.NET 5以降での利用が推奨されます。
用途や環境に応じて適切なモードを選択してください。
パフォーマンス最適化
バッファサイズ調整とスループット
AES暗号化・復号化のパフォーマンスは、データの読み書きに使うバッファサイズの設定によって大きく影響を受けます。
バッファサイズが小さすぎるとI/O回数が増え、オーバーヘッドが大きくなります。
一方、大きすぎるとメモリ消費が増え、システム全体のパフォーマンスに悪影響を及ぼすことがあります。
一般的に、4KB(4096バイト)から64KB程度のバッファサイズがよく使われます。
最適なサイズは環境や処理内容によって異なるため、実際にベンチマークを行い調整することが望ましいです。
以下のポイントを参考にしてください。
- バッファサイズの目安
- 小規模データや低メモリ環境:4KB〜8KB
- 大容量データや高速ストレージ環境:16KB〜64KB
- バッファサイズとスループットの関係
バッファサイズを大きくすると、一度に処理するデータ量が増え、I/O回数が減るためスループットが向上します。
ただし、極端に大きいとメモリ使用量が増え、GC(ガベージコレクション)負荷が高まることがあります。
- ストリーム処理との相性
CryptoStream
を使った暗号化・復号化では、バッファサイズを適切に設定することで、ストリームの読み書き効率が改善されます。
- 例:バッファサイズを指定した読み書き
byte[] buffer = new byte[8192]; // 8KBバッファ
int bytesRead;
while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
cryptoStream.Write(buffer, 0, bytesRead);
}
このようにバッファサイズを調整しながら処理することで、パフォーマンスの最適化が可能です。
大容量ファイル暗号化の分割処理
大容量ファイルを一括でメモリに読み込んで暗号化・復号化するのは非効率であり、メモリ不足や処理遅延の原因になります。
分割処理(チャンク処理)を行うことで、メモリ使用量を抑えつつ安定したパフォーマンスを実現できます。
分割処理のポイントは以下の通りです。
- チャンクサイズの設定
4KB〜64KB程度のチャンクサイズを設定し、ファイルを順次読み込みながら処理します。
チャンクサイズはバッファサイズと同様に環境に応じて調整します。
- ストリームベースの処理
FileStream
やNetworkStream
などのストリームを使い、チャンク単位で読み書きします。
CryptoStream
と組み合わせることで、暗号化・復号化を透過的に行えます。
- パディングと最終ブロックの処理
CryptoStream.FlushFinalBlock()
を必ず呼び出し、パディングの追加や最終ブロックの処理を完了させます。
これを忘れると復号時にエラーが発生します。
- 例:大容量ファイルの暗号化
using (FileStream inputFileStream = new FileStream("largefile.dat", FileMode.Open))
using (FileStream outputFileStream = new FileStream("largefile_encrypted.dat", FileMode.Create))
using (Aes aes = Aes.Create())
{
aes.GenerateKey();
aes.GenerateIV();
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (CryptoStream cryptoStream = new CryptoStream(outputFileStream, encryptor, CryptoStreamMode.Write))
{
byte[] buffer = new byte[8192]; // 8KBチャンク
int bytesRead;
while ((bytesRead = inputFileStream.Read(buffer, 0, buffer.Length)) > 0)
{
cryptoStream.Write(buffer, 0, bytesRead);
}
cryptoStream.FlushFinalBlock();
}
}
- 分割処理のメリット
- メモリ使用量を一定に保てる
- 大容量ファイルでも安定した処理が可能
- ネットワーク越しのストリーム処理にも適用可能
- 注意点
- チャンクの境界でデータが切れても問題ないように、暗号化はストリーム単位で完結させる
- 復号時も同様にチャンク単位で処理し、
FlushFinalBlock()
を忘れない
大容量データの暗号化・復号化は、分割処理とバッファサイズの最適化を組み合わせることで、パフォーマンスと安定性を両立できます。
システムの要件に応じて適切に調整してください。
よくある落とし穴
鍵・IVのハードコーディング
開発時に鍵(Key)や初期化ベクトル(IV)をソースコード内に直接書き込む「ハードコーディング」は非常に危険です。
以下の理由から避けるべきです。
- セキュリティリスクの増大
ソースコードが漏洩した場合、鍵やIVも簡単に取得されてしまい、暗号化の意味がなくなります。
特に公開リポジトリや共有環境では致命的です。
- 鍵の更新が困難
鍵をコードに埋め込むと、鍵を変更するたびにコードの修正・再デプロイが必要になり、運用が煩雑になります。
- 環境ごとの鍵管理ができない
開発・テスト・本番環境で異なる鍵を使いたい場合に柔軟性が失われます。
安全な鍵管理のためには、以下の方法を推奨します。
- 環境変数やセキュアストレージ(例:Azure Key Vault、Windows DPAPI)を利用します
- 鍵管理システム(KMS)を導入し、アプリケーションから安全に取得します
- 鍵は暗号化して保存し、必要時に復号して利用します
固定IV使用による脆弱性
IVは暗号化の初期状態を決める重要なパラメータで、CBCモードなどでは毎回異なるランダムなIVを使うことが必須です。
固定IVを使うと以下のような脆弱性が生じます。
- 同じ平文が同じ暗号文になる
固定IVを使うと、同じ鍵・同じ平文の暗号化結果が毎回同じになります。
これにより、暗号文のパターンが露呈し、攻撃者に情報を推測されやすくなります。
- リプレイ攻撃のリスク増加
同じIVを使い続けると、暗号文の再利用や改ざんが検知しにくくなります。
- セキュリティ標準違反
多くの暗号標準やガイドラインで、IVはランダムかつ一意であることが求められています。
安全なIVの取り扱いポイントは以下です。
- 暗号化ごとにランダムなIVを生成する(
Aes.GenerateIV()
を利用) - IVは秘密にする必要はないが、復号時に必ず同じIVを使うため、暗号文と一緒に保存・送信します
- IVの長さはブロックサイズ(AESは16バイト)に固定します
パディング方式の不一致
AESのブロック暗号は固定長ブロック単位で処理するため、入力データがブロックサイズの倍数でない場合はパディングが必要です。
暗号化と復号化でパディング方式が一致しないと、復号時にエラーが発生します。
よくある問題点は以下の通りです。
- 暗号化時と復号化時で異なるパディング方式を指定している
例えば、暗号化時はPaddingMode.PKCS7
、復号化時はPaddingMode.None
にしてしまうと復号に失敗します。
- パディングが正しく追加・削除されない
独自実装や手動でパディングを操作すると、誤ったパディングが付加されることがあります。
- パディングオラクル攻撃のリスク
パディングエラーの有無で復号結果の違いを攻撃者に知らせてしまうと、パディングオラクル攻撃の対象になります。
対策としては以下を徹底してください。
- 暗号化・復号化で同じパディング方式を使う(通常は
PaddingMode.PKCS7
が推奨) - .NET標準の
Aes
クラスのパディング機能を利用し、手動でパディングを操作しない - 復号時の例外は適切にハンドリングし、攻撃者に情報を与えない
- 認証付き暗号モード(例:GCM)を使い、パディングの問題を回避します
これらの落とし穴を避けることで、安全かつ安定したAES暗号化の実装が可能になります。
鍵共有と配布の選択肢
Password-Based Encryptionでの共有
AESは対称鍵暗号であるため、暗号化と復号化に同じ鍵を使います。
そのため、鍵の安全な共有・配布が重要な課題となります。
パスワードベースの暗号化(Password-Based Encryption、PBE)は、ユーザーが覚えやすいパスワードを使って鍵を生成し、共有する方法の一つです。
PBEでは、パスワードから安全な鍵を派生させるためにPBKDF2(Password-Based Key Derivation Function 2)などの鍵派生関数を使います。
これにより、単純なパスワードを直接鍵として使うリスクを軽減し、推測攻撃に対する耐性を高めます。
PBEの基本的な流れ
- パスワードの入力
共有したいパスワードをユーザーが入力します。
- ソルトの生成
ランダムなソルトを生成し、パスワードと組み合わせて鍵を派生させます。
ソルトはパスワードの同一性による鍵の重複を防ぎます。
- 鍵の派生
PBKDF2などの関数でパスワードとソルトからAES鍵を生成します。
反復回数を多く設定することで総当たり攻撃のコストを増やします。
- 暗号化・復号化
派生した鍵を使ってAES暗号化・復号化を行います。
- ソルトの共有
ソルトは秘密にする必要はなく、暗号文と一緒に保存・送信します。
C#でのPBE例
using System.Security.Cryptography;
using System.Text;
string password = "ユーザーパスワード";
byte[] salt = new byte[16];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
int iterations = 100_000;
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
byte[] key = pbkdf2.GetBytes(32); // 256bit鍵
// このkeyを使ってAES暗号化・復号化を行う
}
PBEはパスワードを使って鍵を共有できるため、鍵の直接配布が難しい環境で有効です。
ただし、パスワードの強度や管理がセキュリティの鍵となるため、強力なパスワードを使い、適切な運用が求められます。
公開鍵でAES鍵をラップするハイブリッド方式
対称鍵の安全な共有が難しい場合、公開鍵暗号を組み合わせたハイブリッド暗号方式がよく使われます。
これは、AESの対称鍵を公開鍵暗号で暗号化(ラップ)し、安全に送信する方法です。
ハイブリッド方式の流れ
- AES鍵の生成
送信者がランダムなAES鍵を生成します。
- AES鍵の公開鍵暗号による暗号化
受信者の公開鍵を使ってAES鍵を暗号化します。
これにより、AES鍵は安全に送信可能になります。
- データのAES暗号化
生成したAES鍵で実際のデータを暗号化します。
- 暗号化データと暗号化されたAES鍵の送信
受信者に暗号化データと暗号化されたAES鍵を送ります。
- 受信者によるAES鍵の復号
受信者は秘密鍵を使ってAES鍵を復号し、データの復号に使います。
メリット
- 高速なデータ暗号化
AESの高速性を活かしつつ、鍵の安全な配布が可能です。
- 公開鍵の安全性
公開鍵暗号は鍵配布問題を解決し、秘密鍵は受信者のみが保持します。
- 広く使われる方式
TLSやPGPなど、多くのセキュリティプロトコルで採用されています。
C#での簡単なイメージ例
using System.Security.Cryptography;
// 送信者側
byte[] aesKey = new byte[32];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(aesKey);
}
// 受信者のRSA公開鍵でAES鍵を暗号化
using (RSA rsa = RSA.Create())
{
// rsa.ImportParameters(受信者の公開鍵パラメータ);
byte[] encryptedAesKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256);
// encryptedAesKeyを送信
}
// 受信者側
using (RSA rsa = RSA.Create())
{
// rsa.ImportParameters(受信者の秘密鍵パラメータ);
byte[] decryptedAesKey = rsa.Decrypt(encryptedAesKey, RSAEncryptionPadding.OaepSHA256);
// decryptedAesKeyを使ってAES復号化
}
このように、AES鍵を公開鍵暗号でラップすることで、安全かつ効率的に鍵を共有できます。
ハイブリッド方式は、対称鍵暗号の高速性と公開鍵暗号の安全な鍵配布を両立させるため、実務で最も一般的な鍵共有手法です。
追加ライブラリ活用
Bouncy Castleの特色と導入
Bouncy Castleは、C#を含む複数のプログラミング言語で利用可能なオープンソースの暗号ライブラリです。
標準の.NET暗号ライブラリでは対応していないアルゴリズムや機能を提供し、柔軟かつ高度な暗号処理を実現できます。
特色
- 豊富な暗号アルゴリズムのサポート
AESはもちろん、RSA、DSA、ECDSA、Blowfish、Twofish、Camelliaなど、多数の対称・非対称暗号をサポートしています。
- 認証付き暗号や特殊モードの実装
GCMやCCMなどの認証付き暗号モード、PKCS#7パディング、PBE(Password-Based Encryption)など、標準ライブラリよりも多彩な機能があります。
- クロスプラットフォーム対応
.NET Framework、.NET Core、Xamarinなど幅広い環境で利用可能です。
- 拡張性とカスタマイズ性
独自の暗号アルゴリズムやプロトコルの実装に適しており、細かいパラメータ調整も可能です。
導入方法
Bouncy CastleのC#版はNuGetパッケージとして提供されています。
導入は以下のコマンドで簡単に行えます。
Install-Package BouncyCastle
または、.NET CLIを使う場合は
dotnet add package BouncyCastle
簡単なAES暗号化例(Bouncy Castle)
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Paddings;
using Org.BouncyCastle.Security;
using System;
using System.Text;
string plainText = "秘密のメッセージ";
byte[] key = new byte[16]; // 128bit鍵
byte[] iv = new byte[16]; // 128bit IV
// ランダム生成
var random = new SecureRandom();
random.NextBytes(key);
random.NextBytes(iv);
var engine = new CbcBlockCipher(new AesEngine());
var cipher = new PaddedBufferedBlockCipher(engine, new Pkcs7Padding());
cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
byte[] inputBytes = Encoding.UTF8.GetBytes(plainText);
byte[] encrypted = cipher.DoFinal(inputBytes);
Console.WriteLine($"暗号化データ(Base64): {Convert.ToBase64String(encrypted)}");
Bouncy Castleは標準ライブラリで不足する機能を補う強力なツールとして、特に特殊な暗号要件やカスタム実装が必要な場合に有効です。
Azure Key Vaultとの連携例
Azure Key VaultはMicrosoft Azureが提供するクラウドベースのシークレット管理サービスで、鍵や証明書、パスワードなどの機密情報を安全に保管・管理できます。
AES鍵の管理にも利用でき、アプリケーションから安全に鍵を取得して暗号化処理に活用できます。
連携のメリット
- 鍵の安全な保管
鍵をクラウド上のセキュアなストレージに保管し、物理的な漏洩リスクを低減。
- アクセス制御と監査
Azure Active Directory(AAD)と連携し、アクセス権限を細かく設定可能です。
操作ログも取得できるためコンプライアンス対応に有効。
- 鍵のローテーション
定期的な鍵の更新や無効化を容易に実施できます。
- ハードウェアセキュリティモジュール(HSM)対応
高度なセキュリティ要件に対応したHSMで鍵を保護。
Azure Key VaultからAES鍵を取得して利用する流れ
- Azure Key Vaultのセットアップ
AzureポータルでKey Vaultを作成し、AES鍵を生成またはインポートします。
- アプリケーションの認証設定
Azure ADでアプリケーション登録を行い、Key Vaultへのアクセス権限を付与します。
- Azure SDKの導入
NuGetでAzure.Security.KeyVault.Keys
やAzure.Identity
パッケージを追加します。
- 鍵の取得と暗号化処理
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using System;
using System.Text;
using System.Threading.Tasks;
async Task EncryptWithKeyVaultAsync()
{
string keyVaultUrl = "https://<your-keyvault-name>.vault.azure.net/";
string keyName = "<your-key-name>";
var client = new KeyClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
KeyVaultKey key = await client.GetKeyAsync(keyName);
var cryptoClient = new CryptographyClient(key.Id, new DefaultAzureCredential());
string plainText = "秘密のメッセージ";
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
// AES鍵のラップ/アンラップや暗号化はKey Vaultの機能に依存しますが、
// ここではRSA鍵での暗号化例を示します。
EncryptResult encryptResult = await cryptoClient.EncryptAsync(EncryptionAlgorithm.RsaOaep, plainBytes);
byte[] cipherBytes = encryptResult.Ciphertext;
Console.WriteLine($"暗号化データ(Base64): {Convert.ToBase64String(cipherBytes)}");
}
EncryptWithKeyVaultAsync().GetAwaiter().GetResult();
注意点
- Azure Key Vaultは対称鍵の直接的な暗号化・復号化機能は限定的で、主に鍵の管理や非対称鍵の操作に使われます。対称鍵(AES鍵)はKey Vaultで安全に保管し、アプリケーション側で取得して暗号化処理に利用する形が一般的です
- 鍵のラップ(WrapKey)/アンラップ(UnwrapKey)機能を使い、AES鍵を公開鍵暗号で保護するハイブリッド方式が推奨されます
Bouncy Castleは高度な暗号機能を補完し、Azure Key Vaultは鍵管理の安全性を高めるための強力なツールです。
これらを組み合わせることで、堅牢かつ運用しやすいAES暗号化システムを構築できます。
テストと検証
公式テストベクターでの動作確認
AES暗号化・復号化の実装が正しく動作しているかを検証するために、公式のテストベクターを使うことが重要です。
テストベクターとは、標準化団体や信頼できる機関が公開している、入力データとそれに対応する正しい暗号文や復号結果のセットです。
これを使うことで、自作の実装が仕様通りに動作しているかを客観的に確認できます。
代表的なテストベクターの入手先
- NIST(米国国立標準技術研究所)
NISTはAESの標準化を行った機関で、公式のテストベクターを公開しています。
例: NIST AES Known Answer Tests (KATs)
- RFC文書
RFC 3602など、AESの利用例を示した文書にもテストベクターが含まれています。
テストベクターの内容例
項目 | 内容(16進数) |
---|---|
鍵(Key) | 2b7e151628aed2a6abf7158809cf4f3c |
初期化ベクトル(IV) | 000102030405060708090a0b0c0d0e0f |
平文(Plaintext) | 6bc1bee22e409f96e93d7e117393172a |
期待される暗号文(Ciphertext) | 7649abac8119b246cee98e9b12e9197d |
実装例:テストベクターを使った検証
using System;
using System.Security.Cryptography;
byte[] key = new byte[] {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c
};
byte[] iv = new byte[] {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
byte[] plaintext = new byte[] {
0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96,
0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a
};
byte[] expectedCiphertext = new byte[] {
0x76, 0x49, 0xab, 0xac, 0x81, 0x19, 0xb2, 0x46,
0xce, 0xe9, 0x8e, 0x9b, 0x12, 0xe9, 0x19, 0x7d
};
byte[] encrypted;
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None; // テストベクターはブロックサイズぴったりのためパディングなし
using (var encryptor = aes.CreateEncryptor())
{
encrypted = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
}
}
bool isMatch = encrypted.Length == expectedCiphertext.Length;
for (int i = 0; i < encrypted.Length && isMatch; i++)
{
if (encrypted[i] != expectedCiphertext[i])
isMatch = false;
}
Console.WriteLine(isMatch ? "テストベクター検証成功" : "テストベクター検証失敗");
テストベクター検証成功
このように、公式テストベクターを使って暗号化結果が期待値と一致するかを確認することで、実装の正確性を保証できます。
ベンチマークでの速度測定
AES暗号化・復号化のパフォーマンスは、システムの要件や処理環境によって重要な評価ポイントです。
実際の環境で速度を測定し、最適化やモード選択の参考にするためにベンチマークを行います。
ベンチマークのポイント
- 測定対象
- 暗号化処理のスループット(秒あたりの処理バイト数)
- 復号化処理のスループット
- CPU使用率やメモリ消費も参考にする場合がある
- テストデータのサイズ
小さなデータ(数KB)から大きなデータ(数MB〜数GB)まで、実際の利用シナリオに合わせて複数サイズで測定します。
- 繰り返し回数
処理時間のばらつきを抑えるため、複数回繰り返して平均値を取ります。
- 環境の固定
CPU負荷や他のプロセスの影響を避けるため、可能な限り安定した環境で測定します。
C#での簡単なベンチマーク例
using System;
using System.Diagnostics;
using System.Security.Cryptography;
byte[] data = new byte[10 * 1024 * 1024]; // 10MBのテストデータ
RandomNumberGenerator.Fill(data);
byte[] key = new byte[32];
byte[] iv = new byte[16];
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(iv);
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
var encryptor = aes.CreateEncryptor();
Stopwatch sw = Stopwatch.StartNew();
byte[] encrypted = encryptor.TransformFinalBlock(data, 0, data.Length);
sw.Stop();
double seconds = sw.Elapsed.TotalSeconds;
double mbPerSec = (data.Length / (1024.0 * 1024.0)) / seconds;
Console.WriteLine($"暗号化時間: {seconds:F3}秒");
Console.WriteLine($"スループット: {mbPerSec:F2} MB/s");
}
暗号化時間: 0.008秒
スループット: 1226.71 MB/s
ベンチマーク結果の活用
- モードや鍵長の選択
AES-128とAES-256で速度差を比較し、パフォーマンスとセキュリティのバランスを検討。
- バッファサイズの調整
ストリーム処理時のバッファサイズを変えて最適な値を探ります。
- ハードウェア支援の有無
AES-NIなどのCPU命令セットが有効かどうかを確認し、環境に応じた最適化を行います。
公式テストベクターによる動作確認とベンチマークによる速度測定は、AES暗号化の実装品質と性能を保証するための基本的かつ重要なステップです。
これらを適切に実施し、安全かつ効率的な暗号化システムを構築しましょう。
モバイル・クロスプラットフォーム対応
Xamarin・MAUIでの鍵保護
XamarinやMAUIはC#を使ってiOSやAndroidなど複数のプラットフォーム向けにアプリを開発できるクロスプラットフォームフレームワークです。
これらの環境でAES暗号化を実装する際、鍵の安全な保護が特に重要になります。
鍵保護の課題
- モバイル端末は物理的に盗難・紛失されやすい
鍵を端末内に平文で保存すると、悪意ある第三者に容易にアクセスされるリスクがあります。
- プラットフォームごとに異なるセキュリティ機構
iOSとAndroidでは鍵管理の仕組みが異なるため、共通のAPIで安全に鍵を扱う必要があります。
Xamarin・MAUIでの鍵保護手法
- Secure Storageの利用
Xamarin.EssentialsやMAUI Essentialsが提供するSecureStorage
APIを使うと、プラットフォームのネイティブな安全領域(iOSのKeychain、AndroidのEncryptedSharedPreferencesやKeystore)にデータを保存できます。
例:AES鍵をBase64エンコードしてSecureStorage
に保存し、必要時に復号して利用します。
using Xamarin.Essentials;
// 鍵の保存
await SecureStorage.SetAsync("aes_key", Convert.ToBase64String(aesKey));
// 鍵の取得
string base64Key = await SecureStorage.GetAsync("aes_key");
byte[] aesKey = Convert.FromBase64String(base64Key);
- プラットフォーム固有のAPIの活用
より高度な鍵管理が必要な場合は、iOSのKeychain ServicesやAndroidのKeystore Systemを直接呼び出すことも可能です。
MAUIやXamarinでは依存サービスやネイティブコード呼び出しで対応します。
- ハードウェアセキュリティの活用
端末が対応していれば、Secure Enclave(iOS)やStrongBox(Android)などのハードウェアセキュリティモジュールを利用し、鍵の安全性を高められます。
注意点
SecureStorage
はユーザーの生体認証やPINロックと連携可能ですが、設定によってはアクセス制限が緩い場合もあるため、アプリのセキュリティ要件に応じて適切に設定してください- 鍵の生成はアプリ起動時や初回利用時に行い、以降は安全に保管・取得する運用が望ましいです
Unity(WebGL)での制限事項
Unityはゲームやインタラクティブコンテンツの開発に広く使われるエンジンで、WebGLビルドによりブラウザ上で動作するアプリケーションを作成できます。
しかし、WebGL環境には特有の制限があり、AES暗号化の実装や鍵管理に注意が必要です。
主な制限事項
- ネイティブ暗号APIの利用不可
WebGLはブラウザのJavaScript環境上で動作するため、.NETのネイティブ暗号ライブラリ(System.Security.Cryptography
など)が利用できません。
代わりにJavaScriptのWeb Crypto APIを呼び出すか、C#で実装された暗号ライブラリを使う必要があります。
- 鍵の安全な保管が困難
ブラウザ環境ではローカルストレージやIndexedDBにデータを保存できますが、これらはユーザーや他のスクリプトからアクセス可能であり、鍵の安全性は保証されません。
- コードの難読化・保護が必須
WebGLビルドはJavaScriptに変換されるため、コードが容易に解析されやすいです。
鍵や重要なロジックは難読化や分割して保護する工夫が必要です。
対応策
- Web Crypto APIの利用
UnityのJavaScriptプラグイン機能を使い、ブラウザのWeb Crypto APIを呼び出してAES暗号化を行う方法があります。
これにより、ブラウザのネイティブ暗号機能を活用できます。
- 鍵の外部管理
鍵をサーバー側で管理し、必要に応じてセッションごとに取得・破棄する方式が推奨されます。
クライアント側に長期間鍵を保持しないことでリスクを軽減します。
- 難読化ツールの活用
UnityのIL2CPPビルドやJavaScript難読化ツールを使い、コード解析を困難にします。
UnityのWebGL環境では、AES暗号化の実装と鍵管理に制約が多いため、以下の点を意識してください。
- ネイティブの.NET暗号ライブラリは使えないため、JavaScriptのWeb Crypto APIやC#のマネージドライブラリを利用します
- 鍵の安全な保管は難しいため、サーバー側で鍵管理を行い、クライアントには必要最小限の情報のみを渡します
- コードの難読化や分割で解析リスクを下げます
これらの対策を講じることで、WebGL環境でも可能な限り安全なAES暗号化を実現できます。
コンフィグファイル暗号化
appsettings.json機密情報の保護
.NETアプリケーションでは、設定情報をappsettings.json
ファイルに記述することが一般的です。
しかし、このファイルにAPIキーやデータベース接続文字列などの機密情報を平文で保存すると、ソースコード管理やデプロイ時に情報漏洩のリスクが高まります。
機密情報を安全に管理するためには、appsettings.json
内の該当部分を暗号化する方法が有効です。
暗号化された情報は、アプリケーション起動時に復号して利用します。
保護すべき情報例
- データベース接続文字列(ConnectionStrings)
- APIキーやシークレット
- 外部サービスの認証情報
暗号化のポイント
- 暗号化対象の限定
ファイル全体を暗号化するのではなく、機密情報の部分だけを暗号化し、他の設定は平文のままにすることが多いです。
- 鍵管理
復号に使う鍵は安全に管理し、環境変数やセキュアストレージから取得するのが望ましいです。
- 運用の自動化
暗号化・復号化の処理をビルドやデプロイのパイプラインに組み込み、手動ミスを防ぎます。
透過的暗号化の構成例
透過的暗号化とは、アプリケーションコードからは通常の設定ファイルとして読み書きでき、内部で自動的に暗号化・復号化が行われる仕組みです。
これにより、開発者は暗号化の詳細を意識せずに機密情報を安全に扱えます。
実装の基本構成
- 暗号化ユーティリティの作成
AESなどを使い、文字列の暗号化・復号化メソッドを用意します。
- カスタム設定プロバイダーの実装
IConfigurationProvider
やIConfigurationSource
を拡張し、appsettings.json
の読み込み時に暗号化された値を復号化して返す仕組みを作ります。
- 設定ファイルの暗号化
機密情報を暗号化ユーティリティで暗号化し、appsettings.json
にBase64などの文字列で保存します。
- アプリケーション起動時の復号化
カスタム設定プロバイダーが暗号化された値を検出し、自動的に復号化してアプリケーションに渡します。
簡単な暗号化ユーティリティ例
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class AesEncryptionHelper
{
private static readonly byte[] Key = Convert.FromBase64String("環境変数などから安全に取得したBase64鍵");
private static readonly byte[] IV = Convert.FromBase64String("同様に取得したBase64 IV");
public static string Encrypt(string plainText)
{
using (Aes aes = Aes.Create())
{
aes.Key = Key;
aes.IV = IV;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform encryptor = aes.CreateEncryptor();
using (MemoryStream ms = new MemoryStream())
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
using (StreamWriter sw = new StreamWriter(cs, Encoding.UTF8))
{
sw.Write(plainText);
sw.Flush();
cs.FlushFinalBlock();
return Convert.ToBase64String(ms.ToArray());
}
}
}
public static string Decrypt(string cipherText)
{
byte[] cipherBytes = Convert.FromBase64String(cipherText);
using (Aes aes = Aes.Create())
{
aes.Key = Key;
aes.IV = IV;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform decryptor = aes.CreateDecryptor();
using (MemoryStream ms = new MemoryStream(cipherBytes))
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (StreamReader sr = new StreamReader(cs, Encoding.UTF8))
{
return sr.ReadToEnd();
}
}
}
}
カスタム設定プロバイダーのイメージ
appsettings.json
の機密情報は以下のようにBase64の暗号文で保存
{
"ConnectionStrings": {
"DefaultConnection": "U2FsdGVkX1+...(暗号化された文字列)"
}
}
- アプリケーションの設定読み込み時に、カスタムプロバイダーが
DefaultConnection
の値を検出し、AesEncryptionHelper.Decrypt
で復号化して返します
利点
- 開発者は平文の設定値を扱う感覚で利用可能です
- 機密情報はファイル上で暗号化されているため、漏洩リスクを低減
- 鍵管理を適切に行えば、セキュリティを高めつつ運用負荷を軽減できます
このように、appsettings.json
の機密情報をAES暗号化で保護し、透過的に復号化する仕組みを導入することで、安全かつ利便性の高い設定管理が実現できます。
認証情報の保護
トークン保存へのAES適用
認証トークン(アクセストークンやリフレッシュトークンなど)は、ユーザーの認証状態を維持するために重要な情報です。
これらのトークンを安全に保存しないと、不正アクセスやなりすましのリスクが高まります。
AES暗号化を用いてトークンを保護することは、クライアントやサーバー側でのセキュリティ強化に有効です。
トークン保存時のAES暗号化のポイント
- 保存場所の選択
- クライアント側(例:モバイルアプリのローカルストレージ、ブラウザのローカルストレージやセッションストレージ)
- サーバー側(例:データベースやキャッシュ)
- 鍵管理
トークンを暗号化するAES鍵は安全に管理し、漏洩しないようにします。
クライアント側ではSecure StorageやKeychain、サーバー側では環境変数やシークレットマネージャーを利用します。
- 暗号化・復号化の実装例
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class TokenEncryption
{
private static readonly byte[] Key = Convert.FromBase64String("Base64で安全に管理された鍵");
private static readonly byte[] IV = Convert.FromBase64String("Base64で安全に管理されたIV");
public static string EncryptToken(string token)
{
using (Aes aes = Aes.Create())
{
aes.Key = Key;
aes.IV = IV;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform encryptor = aes.CreateEncryptor();
using (MemoryStream ms = new MemoryStream())
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
using (StreamWriter sw = new StreamWriter(cs, Encoding.UTF8))
{
sw.Write(token);
sw.Flush();
cs.FlushFinalBlock();
return Convert.ToBase64String(ms.ToArray());
}
}
}
public static string DecryptToken(string encryptedToken)
{
byte[] cipherBytes = Convert.FromBase64String(encryptedToken);
using (Aes aes = Aes.Create())
{
aes.Key = Key;
aes.IV = IV;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform decryptor = aes.CreateDecryptor();
using (MemoryStream ms = new MemoryStream(cipherBytes))
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (StreamReader sr = new StreamReader(cs, Encoding.UTF8))
{
return sr.ReadToEnd();
}
}
}
}
- 運用上の注意
- IVは毎回ランダムに生成し、暗号文と一緒に保存・送信する方法が望ましい。上記例は簡略化のため固定IVを使っていますが、実際は安全なIV管理が必須です
- トークンの有効期限や再発行の仕組みと組み合わせて、暗号化だけに依存しない多層防御を行うことが重要です
セッションデータ暗号化方式
Webアプリケーションや分散システムでは、ユーザーのセッション情報をサーバーやクライアントに保存します。
セッションデータには認証情報やユーザーの状態が含まれるため、暗号化して保護することが推奨されます。
セッションデータ暗号化の方法
- サーバー側セッションストアの暗号化
Redisやデータベースなどのセッションストアに保存する前に、AESでセッションデータを暗号化します。
これにより、ストアの漏洩時にも情報が保護されます。
- クライアント側セッション(Cookieなど)の暗号化
クッキーにセッション情報を保存する場合は、AESで暗号化し、改ざん検知のためにHMACなどの認証タグを付加します。
実装例:AES+HMACによるセッションデータ保護
- セッションデータのシリアライズ
JSONなどで文字列化。
- AESで暗号化
シリアライズした文字列をAESで暗号化。
- HMACで認証タグ生成
暗号文に対してHMAC-SHA256などで認証タグを生成し、改ざん検知を行います。
- 暗号文+認証タグを保存
セッションストアやクッキーに保存。
- 復号時に認証タグ検証
改ざんがなければ復号し、セッション情報を復元。
C#での簡単なイメージコード
// 省略:AES暗号化・復号化は前述のTokenEncryptionクラスを利用
using System.Security.Cryptography;
public static class SessionProtection
{
private static readonly byte[] HmacKey = Convert.FromBase64String("HMAC用の安全な鍵");
public static byte[] ComputeHmac(byte[] data)
{
using (var hmac = new HMACSHA256(HmacKey))
{
return hmac.ComputeHash(data);
}
}
public static bool VerifyHmac(byte[] data, byte[] expectedHmac)
{
using (var hmac = new HMACSHA256(HmacKey))
{
byte[] computed = hmac.ComputeHash(data);
return CryptographicOperations.FixedTimeEquals(computed, expectedHmac);
}
}
}
- 運用上のポイント
- AES鍵とHMAC鍵は別々に管理し、漏洩リスクを分散します
- 認証タグの検証に失敗した場合はセッションを破棄し、不正アクセスを防止
- セッションの有効期限管理や再生成も併せて実装します
トークンやセッションデータの暗号化は、認証情報の漏洩リスクを大幅に低減し、堅牢なセキュリティを実現します。
AES暗号化と認証付き検証を組み合わせ、適切な鍵管理と運用ルールを守ることが重要です。
法規制と標準準拠
NIST SP 800-38Aを満たすポイント
NIST(米国国立標準技術研究所)が発行するSP 800-38Aは、ブロック暗号モードの標準仕様を定めた文書であり、AESを含むブロック暗号の安全な利用に関するガイドラインを提供しています。
AES暗号化を実装する際にこの標準に準拠することは、セキュリティの信頼性を高めるうえで重要です。
主なポイント
- 暗号モードの選択
SP 800-38Aでは、CBC(Cipher Block Chaining)、ECB(Electronic Codebook)、CFB(Cipher Feedback)、OFB(Output Feedback)、CTR(Counter)などのモードが規定されています。
- CBCやCTRは広く使われており、特にCBCは多くのシステムで標準的に採用されています
- ECBは同じ平文ブロックが同じ暗号文になるため推奨されません
- 初期化ベクトル(IV)の管理
- IVはランダムかつ一意でなければならず、再利用を避けること
- IVの長さはブロックサイズ(AESは128ビット)に固定
- IVは暗号文と一緒に保存・送信し、復号時に使用
- パディング方式
- パディングはPKCS#7など標準的な方式を使うこと
- パディングの不一致は復号エラーやセキュリティリスクにつながるため注意
- 鍵長の選択
- AES-128、AES-192、AES-256のいずれかを使用
- セキュリティ要件に応じて適切な鍵長を選択
- 認証付き暗号の推奨
- SP 800-38A自体は認証付き暗号を規定していませんが、改ざん検知のためにAES-GCMなどの認証付き暗号モードの利用が推奨されます
- 鍵管理の重要性
- 鍵の生成、配布、保管、破棄を安全に行うこと
- 鍵の漏洩は暗号の安全性を根本から損なうため、厳格な管理が必要でしょう
実装上の注意
- .NETの
Aes
クラスを使う場合、Mode
プロパティでCBCやGCMを指定し、Padding
プロパティでPKCS7を設定することで標準に準拠した暗号化が可能です - IVは毎回ランダム生成し、暗号文と一緒に保存・送信する設計にします
- 復号時は必ずパディングエラーや認証タグの検証を行い、不正なデータを検知します
GDPR・PCI-DSSとAES利用
AES暗号化は、個人情報保護や決済カード情報の安全管理に関する国際的な法規制や業界標準でも広く認められています。
特にGDPR(EU一般データ保護規則)やPCI-DSS(Payment Card Industry Data Security Standard)では、適切な暗号化の実施が求められています。
GDPRにおけるAESの役割
- 個人データの保護
GDPRはEU域内の個人データの取り扱いに厳しい規制を設けており、データ漏洩時の罰則も厳格です。
AESは個人データの暗号化に推奨される技術の一つです。
- 技術的・組織的対策の一環
AES暗号化は「適切な技術的対策」として位置づけられ、データの機密性を確保します。
- データ漏洩通知の免除条件
暗号化されたデータが漏洩しても、鍵が安全に管理されていれば通知義務が免除される場合があります。
- 鍵管理の重要性
鍵の管理が不十分だと暗号化の効果が薄れるため、GDPR準拠には鍵管理ポリシーの整備も必要です。
PCI-DSSにおけるAESの利用
- カード会員データの保護
PCI-DSSはクレジットカード情報の保護基準であり、保存・送信されるカードデータの暗号化を義務付けています。
- 強力な暗号化アルゴリズムの使用
AESはPCI-DSSで推奨される暗号化アルゴリズムの一つであり、128ビット以上の鍵長が求められます。
- 鍵管理要件
- 鍵の生成、配布、保管、ローテーション、破棄を厳格に管理すること
- 鍵へのアクセスは必要最小限に制限し、監査ログを残すこと
- 通信の暗号化
ネットワーク上のカードデータ送信にはTLSなどの暗号化プロトコルを使い、AESがその基盤技術として利用されることが多いです。
実務での対応例
- AES-256を使い、CBCまたはGCMモードで暗号化を実施
- 鍵はHSM(ハードウェアセキュリティモジュール)やクラウドの鍵管理サービスで安全に管理
- 定期的な鍵ローテーションとアクセス制御を実施
- 暗号化・復号化処理のログを取得し、監査に備えます
NIST SP 800-38Aの標準に準拠しつつ、GDPRやPCI-DSSの要件を満たすAES暗号化の実装は、法規制対応とセキュリティ強化の両面で不可欠です。
これらの基準を踏まえた設計・運用を行うことで、安全かつコンプライアンスに適合したシステムを構築できます。
進化する暗号化トレンド
Post-Quantum時代におけるAES
量子コンピュータの発展により、従来の公開鍵暗号が脅かされる「Post-Quantum(ポスト量子)時代」が近づいています。
量子アルゴリズムの一つであるショアのアルゴリズムはRSAやECCなどの公開鍵暗号を効率的に解読できるため、これらの暗号方式は将来的に安全性が低下するリスクがあります。
一方、AESのような対称鍵暗号は量子コンピュータの影響を受けにくいとされています。
量子アルゴリズムのグローバーのアルゴリズムは、対称鍵暗号の鍵探索を平方根の時間に短縮しますが、これは鍵長を倍にすることで対抗可能です。
AESの耐量子性
- 鍵長の強化
AES-128は量子攻撃により実質的に64ビットの安全性になるため、Post-Quantum時代にはAES-256の使用が推奨されます。
AES-256は量子攻撃に対して128ビットの安全性を維持できると考えられています。
- 対称鍵暗号の継続利用
公開鍵暗号の代替として、対称鍵暗号は引き続き重要な役割を果たします。
特に大量データの高速暗号化に適しているため、Post-Quantum時代でもAESは中心的な存在です。
- ハイブリッド暗号の見直し
公開鍵暗号部分はポスト量子暗号(PQC)に置き換えられる動きがありますが、AESなどの対称鍵暗号はそのまま利用されるケースが多いです。
実務的な対応策
- 新規システムではAES-256を標準とし、鍵管理を強化します
- ポスト量子暗号の標準化動向(NISTのPQCコンペティションなど)を注視し、公開鍵暗号部分のアップデートに備えます
- 既存システムは段階的に鍵長を強化し、将来的な移行計画を策定します
AES-NIハードウェア支援の活用
AES-NI(AES New Instructions)は、IntelやAMDのCPUに搭載されているAES暗号化処理を高速化するためのハードウェア命令セットです。
AES-NIを活用することで、ソフトウェアベースのAES暗号化に比べて大幅なパフォーマンス向上とセキュリティ強化が可能になります。
AES-NIの特徴
- 高速な暗号化・復号化
AESの主要な演算をCPUの専用命令で実行するため、数倍から十数倍の高速化が期待できます。
- サイドチャネル攻撃耐性の向上
ハードウェア実装により、タイミング攻撃やキャッシュ攻撃などのサイドチャネル攻撃に対する耐性が高まります。
- 透過的な利用
.NETのAes
クラスは環境に応じてAES-NIを自動的に利用するため、開発者が特別な設定をしなくても恩恵を受けられます。
AES-NIの利用状況確認
Windows環境では、System.Security.Cryptography
名前空間のAES実装がAES-NIをサポートしています。
LinuxやmacOSでも対応が進んでいます。
パフォーマンス向上の実例
- 大容量ファイルの暗号化処理で、AES-NI対応CPUでは数百MB/s以上のスループットが実現可能です
- TLS通信などリアルタイム暗号化処理でレイテンシ低減に寄与
開発者ができること
- 最新の.NETランタイムを利用し、AES-NI対応の恩恵を受けます
- CPUのAES-NI対応状況を確認し、非対応環境ではソフトウェア実装のパフォーマンスを考慮します
- パフォーマンスが重要なシナリオでは、ベンチマークを実施しAES-NIの効果を検証します
Post-Quantum時代に備えた鍵長の強化と、AES-NIによるハードウェア支援の活用は、AES暗号化の安全性と効率性を両立させるための重要なトレンドです。
これらを踏まえた設計・運用で、将来にわたって信頼性の高い暗号化システムを維持できます。
参考コードの構成例
プロジェクトレイアウト案
AES暗号化・復号化を含むセキュアなアプリケーションを開発する際は、コードの保守性や拡張性を考慮したプロジェクト構成が重要です。
以下は一般的なC#プロジェクトにおける暗号化機能のレイアウト例です。
/MyApp
├── /MyApp.Core
│ ├── /Services
│ │ └── IEncryptionService.cs // 暗号化サービスのインターフェース
│ ├── /Models
│ │ └── EncryptionSettings.cs // 鍵やIVなどの設定モデル
│ └── /Utils
│ └── AesEncryptionHelper.cs // AES暗号化・復号化のユーティリティクラス
├── /MyApp.Infrastructure
│ ├── /Services
│ │ └── EncryptionService.cs // IEncryptionServiceの実装
│ └── /Configuration
│ └── EncryptionConfigProvider.cs // 設定ファイルから暗号化設定を読み込むクラス
├── /MyApp.WebApi
│ ├── Program.cs
│ ├── Startup.cs
│ └── /Controllers
│ └── SecureDataController.cs // 暗号化サービスを利用するAPIコントローラー
└── /MyApp.Tests
└── EncryptionServiceTests.cs // 暗号化サービスの単体テスト
ポイント
- 分離された責務
暗号化ロジックはCore
層のユーティリティやインターフェースにまとめ、実装はInfrastructure
層に置くことで、依存関係を明確にします。
- 設定管理の分離
鍵やIVなどの設定は専用のモデルや設定プロバイダーで管理し、環境ごとに切り替えやすくします。
- テスト容易性
インターフェースを使いDI(依存性注入)を活用することで、モックを使った単体テストが容易になります。
- API層での利用
Web APIやUI層は暗号化サービスのインターフェースに依存し、実装の詳細を意識せずに利用可能です。
DIコンテナで暗号化サービスを登録
依存性注入(DI)コンテナを使うことで、暗号化サービスの実装を柔軟に切り替えたり、テスト時にモックを差し替えたりできます。
以下はASP.NET CoreのDIコンテナに暗号化サービスを登録する例です。
インターフェース定義例
public interface IEncryptionService
{
string Encrypt(string plainText);
string Decrypt(string cipherText);
}
実装例
public class EncryptionService : IEncryptionService
{
private readonly byte[] _key;
private readonly byte[] _iv;
public EncryptionService(IOptions<EncryptionSettings> options)
{
_key = Convert.FromBase64String(options.Value.Key);
_iv = Convert.FromBase64String(options.Value.IV);
}
public string Encrypt(string plainText)
{
// AES暗号化処理(省略)
}
public string Decrypt(string cipherText)
{
// AES復号化処理(省略)
}
}
DIコンテナへの登録(Startup.csやProgram.cs)
public void ConfigureServices(IServiceCollection services)
{
// 設定ファイルからEncryptionSettingsをバインド
services.Configure<EncryptionSettings>(Configuration.GetSection("EncryptionSettings"));
// IEncryptionServiceの実装を登録
services.AddScoped<IEncryptionService, EncryptionService>();
// その他のサービス登録
services.AddControllers();
}
利用例(コントローラー)
[ApiController]
[Route("[controller]")]
public class SecureDataController : ControllerBase
{
private readonly IEncryptionService _encryptionService;
public SecureDataController(IEncryptionService encryptionService)
{
_encryptionService = encryptionService;
}
[HttpPost("encrypt")]
public IActionResult EncryptData([FromBody] string plainText)
{
var encrypted = _encryptionService.Encrypt(plainText);
return Ok(encrypted);
}
[HttpPost("decrypt")]
public IActionResult DecryptData([FromBody] string cipherText)
{
var decrypted = _encryptionService.Decrypt(cipherText);
return Ok(decrypted);
}
}
メリット
- 疎結合な設計
実装の詳細に依存せず、インターフェース経由で利用できるため、将来的な実装変更が容易。
- テストの容易さ
テスト時にモック実装をDIコンテナに登録し、暗号化処理を模擬可能です。
- 設定の一元管理
鍵やIVなどの設定をIOptions<T>
で管理し、環境ごとに切り替えやすい。
このように、プロジェクト構成を整理し、DIコンテナで暗号化サービスを登録・利用することで、保守性・拡張性に優れたAES暗号化機能を実装できます。
デバッグとトラブルシュート
CryptographicException原因別対処
CryptographicException
は、.NETの暗号化処理でよく発生する例外の一つで、AES暗号化・復号化時にも頻繁に遭遇します。
原因は多岐にわたるため、発生時には原因を特定し適切に対処することが重要です。
主な原因と対処法
原因 | 説明 | 対処法 |
---|---|---|
鍵(Key)や初期化ベクトル(IV)の不一致 | 暗号化時と復号化時で異なる鍵やIVを使用している | 鍵・IVの管理を見直し、暗号化・復号化で同一の値を使う |
パディングエラー | パディング方式の不一致や不正なパディングデータ | 暗号化・復号化で同じパディング方式を設定し、データ破損を防ぐ |
データ破損・改ざん | 暗号文が途中で変更・破損している | データの整合性チェック(HMACや認証付き暗号)を導入し、改ざん検知を行う |
不正な入力データ | 空のバイト配列やnull値を渡している | 入力値の検証を行い、nullや空配列を排除する |
ストリームの不適切な操作 | CryptoStream のFlushFinalBlock() を呼ばないなど | 書き込み完了時に必ずFlushFinalBlock() を呼び、パディング処理を完了させる |
例外発生時のログ例
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
at System.Security.Cryptography.AesCryptoServiceProvider.CreateDecryptor(Byte[] rgbKey, Byte[] rgbIV)
...
この例はパディングエラーを示しており、パディング方式の不一致やデータ破損が疑われます。
デバッグのポイント
- 例外メッセージを詳細に確認し、パディング関連か鍵関連かを切り分けます
- 暗号化・復号化の鍵・IVが一致しているかをログやデバッガで検証
- 入力データの長さや内容を確認し、破損や不正なデータが混入していないか調査
CryptoStream
の使い方を見直し、正しいストリーム操作が行われているか確認
パディングエラーの追跡手順
パディングエラーはCryptographicException
の中でも特に多いトラブルで、復号化時に「Padding is invalid and cannot be removed.」というメッセージで発生します。
原因を特定し、修正するための追跡手順を紹介します。
暗号化・復号化のパディング方式を確認
- 暗号化時と復号化時で同じ
PaddingMode
(通常はPKCS7
)を使っているか確認 - 片方だけ
PaddingMode.None
など異なる設定になっていないかチェック
入力データの整合性を検証
- 復号化対象の暗号文が完全かつ正しいかを確認。途中で切れていたり、改ざんされていないか調査
- Base64エンコード・デコードのミスがないかもチェック
鍵とIVの一致を確認
- 暗号化時と復号化時の鍵・IVが完全に一致しているかをログやデバッガで検証
- IVは毎回ランダム生成し、暗号文と一緒に保存・送信しているか確認
CryptoStreamの使用方法を見直す
- 書き込み時に
FlushFinalBlock()
を必ず呼んでいるか確認。呼ばないとパディングが正しく処理されません - ストリームの閉じ忘れや途中での破棄がないかチェック
テストベクターで動作確認
- 公式のAESテストベクターを使い、暗号化・復号化が正しく動作するか検証
- 自作データで問題が起きる場合は、テストベクターで正常動作するか比較
ログと例外情報の活用
- 例外発生箇所のスタックトレースを詳細に確認
- 可能であれば、暗号化・復号化の中間データをログに出力し、どの段階で異常が起きているか特定
これらの手順を踏むことで、パディングエラーの原因を効率的に特定し、修正できます。
暗号化処理は細かな設定ミスやデータ破損が致命的なエラーにつながるため、丁寧なデバッグが不可欠です。
CI/CDパイプラインでの鍵管理
シークレットスキャンの自動化
CI/CDパイプラインにおける鍵管理で最も重要なポイントの一つが、機密情報の漏洩防止です。
ソースコードや設定ファイルに誤って鍵やパスワードを含めてしまうと、リポジトリの公開やビルドログの保存などを通じて情報が漏洩するリスクがあります。
これを防ぐために「シークレットスキャン」の自動化が不可欠です。
シークレットスキャンとは
シークレットスキャンは、ソースコードやコミット内容、設定ファイルなどに含まれるAPIキー、パスワード、暗号鍵などの機密情報を自動的に検出する仕組みです。
CI/CDパイプラインのビルドやプルリクエストの段階でスキャンを実行し、問題があればビルドを失敗させたり通知を行ったりします。
主なツール・サービス
- GitHub Secret Scanning
GitHubが提供するリポジトリ内のシークレット検出機能。
プライベートリポジトリでも利用可能です。
- GitLab Secret Detection
GitLab CI/CDに組み込めるシークレット検出機能。
- TruffleHog
高度な正規表現やヒューリスティックでシークレットを検出するオープンソースツール。
- Detect Secrets
Yelpが開発したPythonベースのシークレットスキャナー。
自動化のポイント
- CI/CDパイプラインに組み込む
プルリクエストやマージ前に必ずスキャンを実行し、問題があれば開発者にフィードバック。
- 誤検知のチューニング
正当な文字列を誤って検出しないようにホワイトリストやルールの調整を行います。
- スキャン結果の管理
検出されたシークレットは速やかに対応し、漏洩リスクを最小化。
- 教育と運用ルールの整備
開発者に対してシークレットの取り扱いルールを周知し、誤って鍵をコミットしない文化を醸成。
署名付き変数でのKey/IV注入
CI/CDパイプラインで安全にAESの鍵(Key)や初期化ベクトル(IV)を扱うには、環境変数やパイプライン変数として管理し、ビルドやデプロイ時に注入する方法が一般的です。
これらの変数は「署名付き変数」や「シークレット変数」として扱い、アクセス制御や暗号化が施されています。
署名付き変数とは
署名付き変数は、CI/CDツールが提供する機密情報管理機能の一つで、以下の特徴があります。
- 暗号化保存
変数の値は暗号化されて保存され、外部から直接参照できません。
- アクセス制御
変数にアクセスできるユーザーやジョブを限定可能です。
- ログ非表示
変数の値はビルドログに表示されず、漏洩リスクを低減。
- 署名検証
変数の改ざんを防ぐために署名やハッシュで整合性を検証する場合もあります。
具体的な利用例
- Azure DevOps
「ライブラリ」の「変数グループ」でシークレット変数を登録し、パイプラインで参照。
- GitHub Actions
「Secrets」に鍵やIVを登録し、ワークフロー内で${{ secrets.KEY_NAME }}
として利用。
- GitLab CI/CD
「CI/CD設定」の「変数」でシークレット変数を設定し、ジョブ内で環境変数として利用。
Key/IV注入の流れ
- 鍵・IVを安全に生成・登録
事前に安全な方法で生成したAES鍵やIVをCI/CDツールのシークレット変数に登録。
- ビルド・デプロイ時に環境変数として注入
パイプライン実行時に環境変数として注入し、アプリケーションのビルドや設定ファイル生成に利用。
- アプリケーション起動時に環境変数から取得
実行環境で環境変数を読み込み、暗号化サービスに渡して利用。
サンプル(GitHub Actions)
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
env:
AES_KEY: ${{ secrets.AES_KEY }}
AES_IV: ${{ secrets.AES_IV }}
steps:
- uses: actions/checkout@v2
- name: Build
run: dotnet build
- name: Run Tests
run: dotnet test
- name: Deploy
run: |
dotnet run -- --key $AES_KEY --iv $AES_IV
注意点
- シークレット変数は必要最低限の権限で管理し、不要になったら速やかに削除
- 変数の値はログに出力しないように注意し、誤って漏洩しない運用ルールを徹底
- 鍵のローテーションを定期的に行い、長期間の同一鍵使用を避けます
CI/CDパイプラインでの鍵管理は、セキュリティの要であり、シークレットスキャンの自動化と署名付き変数による安全な注入を組み合わせることで、漏洩リスクを最小限に抑えつつ効率的な運用が可能になります。
まとめ
この記事では、C#でのAES暗号化・復号化の実装手順から鍵管理、暗号モードの選択、パフォーマンス最適化、トラブルシュート、法規制対応、最新トレンドまで幅広く解説しました。
安全な鍵・IVの生成と管理、適切なパディングや認証付き暗号の利用、CI/CD環境でのシークレット管理が重要です。
これらを踏まえた設計・運用で、堅牢かつ効率的なAES暗号化システムを構築できます。