文字列

【C#】AESで安全なファイル暗号化と復号を行う実装手順と鍵管理のコツ

ファイルを安全に扱うなら、まずAESなどの共通鍵方式をAesクラスで実装し、鍵とIVを別保管するのが基本です。

Windows限定の簡易手段としてFile.EncryptやDPAPIも選択でき、公開鍵方式は鍵共有時のみ併用すると効率的です。

AES暗号化の基本

ファイルの暗号化を行う際に、まず理解しておきたいのがAES(Advanced Encryption Standard)という暗号方式です。

AESは共通鍵暗号方式の一つで、現在もっとも広く使われている暗号アルゴリズムの一つです。

ここでは、AESの基本的な仕組みや特徴、C#で利用できるクラスについて詳しく解説します。

共通鍵暗号とは

共通鍵暗号とは、暗号化と復号化に同じ鍵を使う暗号方式のことです。

英語では「Symmetric Key Cryptography」と呼ばれます。

暗号化する側と復号化する側が同じ鍵を共有しているため、鍵の管理が非常に重要になります。

共通鍵暗号の特徴は以下の通りです。

  • 高速処理が可能

公開鍵暗号方式に比べて計算コストが低いため、大量のデータを高速に暗号化・復号化できます。

  • 鍵の共有が課題

同じ鍵を安全に共有しなければならず、鍵の漏洩がセキュリティリスクになります。

  • 用途

ファイルの暗号化や通信の暗号化など、データの機密性を保つために広く使われています。

AESはこの共通鍵暗号の代表的なアルゴリズムであり、強力なセキュリティと高速な処理性能を両立しています。

AESの仕組み

AESはブロック暗号の一種で、固定長のデータブロックを一定の鍵長で暗号化します。

AESはアメリカ国立標準技術研究所(NIST)によって標準化されており、世界中で広く採用されています。

ブロックサイズとキー長

AESの特徴的な仕様は以下の通りです。

項目サイズ(ビット)説明
ブロックサイズ1281回の暗号化で処理するデータの単位
鍵長128, 192, 256鍵の長さ。長いほど安全性が高い

AESは常に128ビット(16バイト)のブロック単位でデータを処理します。

鍵長は128ビット、192ビット、256ビットの3種類があり、256ビットが最も強力です。

一般的には256ビットの鍵を使うことが推奨されています。

CBCとGCMの違い

AESは単体ではブロック暗号のアルゴリズムですが、実際の暗号化では「モード」と呼ばれる動作方式を指定します。

代表的なモードにCBC(Cipher Block Chaining)とGCM(Galois/Counter Mode)があります。

モード特徴メリットデメリット
CBC各ブロックの暗号化に前のブロックの暗号文を利用実装が簡単で広く使われている認証機能がないため改ざん検知ができない
GCMカウンタモードに認証タグを付加した認証付き暗号改ざん検知が可能で高速実装がやや複雑
  • CBCモードは、前の暗号化ブロックの結果を次のブロックの暗号化に利用するため、連鎖的に暗号化が行われます。これにより、同じ平文でも異なる暗号文になります。ただし、改ざん検知機能はないため、別途MAC(Message Authentication Code)を付ける必要があります
  • GCMモードは、暗号化と同時に認証タグを生成し、データの改ざんを検知できます。高速かつ安全性が高いため、最近のシステムではGCMが推奨されることが多いです

C#標準ライブラリで利用できるクラス

C#では、.NETの標準ライブラリにAES暗号化をサポートするクラスが用意されています。

主に以下のクラスを使ってAES暗号化・復号化を実装します。

クラス名説明
AesAESアルゴリズムの抽象クラス。Create()でインスタンスを生成
AesCryptoServiceProviderWindowsのCryptoAPIを利用したAES実装(古い)
AesManagedマネージコードで実装されたAES(非推奨)
AesCngWindows CNG(Cryptography Next Generation)を利用したAES

現在はAes.Create()メソッドでAESのインスタンスを生成し、ICryptoTransformを使って暗号化・復号化を行うのが一般的です。

AesCryptoServiceProviderAesManagedは古い実装であり、将来的には非推奨になる可能性があります。

代表的なプロパティ

  • Key:暗号化・復号化に使う鍵(バイト配列)
  • IV:初期化ベクトル(バイト配列)
  • Mode:暗号化モード(例:CipherMode.CBCCipherMode.GCM)
  • Padding:パディング方式(例:PaddingMode.PKCS7)

使い方の流れ

  1. Aes.Create()でAESインスタンスを生成
  2. KeyIVを設定(自動生成も可能)
  3. CreateEncryptor()またはCreateDecryptor()で変換器を作成
  4. CryptoStreamを使ってファイルやストリームを暗号化・復号化

これらのクラスを使うことで、複雑な暗号化処理を簡単に実装できます。

実装全体の流れ

鍵とIVの生成フロー

AES暗号化で重要なのは、暗号化・復号化に使う「鍵(Key)」と「初期化ベクトル(IV)」の生成です。

鍵は暗号の強度を決める要素であり、IVは暗号化の安全性を高めるために使います。

安全な鍵とIVの生成は、暗号化の根幹を支えます。

  • 鍵の生成

鍵はランダムなバイト列で、AESの鍵長(128、192、256ビット)に合わせて生成します。

C#ではRandomNumberGeneratorクラスを使うと安全に乱数を生成できます。

例:256ビット鍵なら32バイトの乱数を生成します。

  • IVの生成

IVはAESのブロックサイズ(128ビット=16バイト)に合わせて生成します。

こちらもRandomNumberGeneratorで安全に生成します。

IVは暗号化ごとに変える必要があり、同じ鍵でも異なるIVを使うことで同じ平文でも異なる暗号文になります。

  • 鍵とIVの管理

鍵は暗号化・復号化の両方で同じものを使うため、安全に保管・共有する必要があります。

IVは暗号文と一緒に保存することが多く、暗号化ファイルの先頭に付加する方法が一般的です。

以下は鍵とIVを生成するサンプルコードです。

using System.Security.Cryptography;
public static class KeyIvGenerator
{
    public static void GenerateKeyAndIv(out byte[] key, out byte[] iv)
    {
        using (var rng = RandomNumberGenerator.Create())
        {
            key = new byte[32]; // 256ビット鍵
            iv = new byte[16];  // 128ビットIV
            rng.GetBytes(key);
            rng.GetBytes(iv);
        }
    }
}

このコードは安全な乱数生成器を使い、鍵とIVをそれぞれ適切な長さで生成しています。

ファイル読み込みから暗号化までのシーケンス

ファイルをAESで暗号化する際の基本的な流れは以下の通りです。

  1. 鍵とIVの準備

事前に生成した鍵とIVを用意します。

鍵は安全に保管し、IVは暗号化ごとに新しく生成します。

  1. 入力ファイルの読み込み

暗号化したいファイルをFileStreamで開きます。

  1. 出力ファイルの作成

暗号化後のデータを書き込むためのFileStreamを作成します。

  1. CryptoStreamの作成

AESのCreateEncryptorメソッドで暗号化用のICryptoTransformを作成し、CryptoStreamに渡します。

CryptoStreamはストリームに対して暗号化処理を行います。

  1. IVの書き込み

復号時に必要なIVは、暗号化ファイルの先頭に書き込みます。

これにより復号時にIVを読み取れます。

  1. ファイルのコピーと暗号化

入力ファイルの内容をCryptoStreamにコピーすると、自動的に暗号化されて出力ファイルに書き込まれます。

  1. ストリームのクローズ

すべてのストリームを閉じてリソースを解放します。

以下はファイルをAESで暗号化するサンプルコードです。

using System;
using System.IO;
using System.Security.Cryptography;
public class AesFileEncryptor
{
    public static void EncryptFile(string inputFile, string outputFile, byte[] key)
    {
        using (Aes aes = Aes.Create())
        {
            aes.Key = key;
            aes.GenerateIV(); // 新しいIVを生成
            using (FileStream fsOut = new FileStream(outputFile, FileMode.Create))
            {
                // IVをファイルの先頭に書き込む
                fsOut.Write(aes.IV, 0, aes.IV.Length);
                using (CryptoStream cs = new CryptoStream(fsOut, aes.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    using (FileStream fsIn = new FileStream(inputFile, FileMode.Open))
                    {
                        fsIn.CopyTo(cs);
                    }
                }
            }
        }
    }
}

このコードでは、暗号化ファイルの先頭にIVを書き込み、その後に暗号化データを書き込んでいます。

復号時にIVを読み取ることで正しく復号できます。

復号処理の逆シーケンス

復号処理は暗号化の逆の流れで行います。

ポイントは暗号化ファイルの先頭に保存されているIVを正しく読み取ることです。

  1. 鍵の準備

暗号化時と同じ鍵を用意します。

  1. 暗号化ファイルの読み込み

FileStreamで暗号化ファイルを開きます。

  1. IVの読み込み

ファイルの先頭16バイト(128ビット)を読み込み、復号用のIVとして設定します。

  1. CryptoStreamの作成

AESのCreateDecryptorに鍵とIVを渡し、復号用のICryptoTransformを作成します。

これを使ってCryptoStreamを作成します。

  1. 復号データの読み込み

CryptoStreamから復号されたデータを読み取り、復号後のファイルに書き込みます。

  1. ストリームのクローズ

すべてのストリームを閉じてリソースを解放します。

以下は復号処理のサンプルコードです。

using System;
using System.IO;
using System.Security.Cryptography;
public class AesFileDecryptor
{
    public static void DecryptFile(string inputFile, string outputFile, byte[] key)
    {
        using (FileStream fsIn = new FileStream(inputFile, FileMode.Open))
        {
            byte[] iv = new byte[16];
            // ファイル先頭からIVを読み込む
            fsIn.Read(iv, 0, iv.Length);
            using (Aes aes = Aes.Create())
            {
                aes.Key = key;
                aes.IV = iv;
                using (CryptoStream cs = new CryptoStream(fsIn, aes.CreateDecryptor(), CryptoStreamMode.Read))
                {
                    using (FileStream fsOut = new FileStream(outputFile, FileMode.Create))
                    {
                        cs.CopyTo(fsOut);
                    }
                }
            }
        }
    }
}

このコードは暗号化ファイルの先頭からIVを読み取り、復号処理に利用しています。

復号後のデータは指定したファイルに書き込まれます。

これらの流れを理解し、鍵とIVの管理を適切に行うことで、安全かつ効率的なAESによるファイル暗号化・復号化が実現できます。

実行できる形にまとめた完全なサンプルコードはこちらです。

using System;
using System.IO;
using System.Security.Cryptography;

public class AesFileEncryptor
{
    public static void GenerateKeyAndIv(out byte[] key, out byte[] iv)
    {
        using (var rng = RandomNumberGenerator.Create())
        {
            key = new byte[32]; // 256ビット鍵
            iv = new byte[16];  // 128ビットIV
            rng.GetBytes(key);
            rng.GetBytes(iv);
        }
    }

    public static void EncryptFile(string inputFile, string outputFile, byte[] key)
    {
        using (Aes aes = Aes.Create())
        {
            aes.Key = key;
            aes.GenerateIV(); // 新しいIVを生成
            using (FileStream fsOut = new FileStream(outputFile, FileMode.Create))
            {
                // IVをファイルの先頭に書き込む
                fsOut.Write(aes.IV, 0, aes.IV.Length);
                using (CryptoStream cs = new CryptoStream(fsOut, aes.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    using (FileStream fsIn = new FileStream(inputFile, FileMode.Open))
                    {
                        fsIn.CopyTo(cs);
                    }
                }
            }
        }
    }

    public static void DecryptFile(string inputFile, string outputFile, byte[] key)
    {
        using (FileStream fsIn = new FileStream(inputFile, FileMode.Open))
        {
            byte[] iv = new byte[16];
            // ファイル先頭からIVを読み込む
            int bytesRead = fsIn.Read(iv, 0, iv.Length);
            if (bytesRead < iv.Length)
                throw new Exception("ファイルが破損しているか、IVの読み込みに失敗しました。");

            using (Aes aes = Aes.Create())
            {
                aes.Key = key;
                aes.IV = iv;
                using (CryptoStream cs = new CryptoStream(fsIn, aes.CreateDecryptor(), CryptoStreamMode.Read))
                {
                    using (FileStream fsOut = new FileStream(outputFile, FileMode.Create))
                    {
                        cs.CopyTo(fsOut);
                    }
                }
            }
        }
    }

    // Mainメソッドを追加
    public static void Main()
    {
        string originalFile = "test.txt";
        string encryptedFile = "test.enc";
        string decryptedFile = "test_dec.txt";

        // テスト用のテキストを作成
        File.WriteAllText(originalFile, "これはAESで暗号化されたテキストのテストです。");

        // 鍵とIVを生成(DecryptFileではIVはファイル先頭に書き込んで読み込むため、生成したIVを使わない)
        GenerateKeyAndIv(out byte[] key, out byte[] iv);

        try
        {
            // ファイルを暗号化
            EncryptFile(originalFile, encryptedFile, key);
            Console.WriteLine("暗号化完了: " + encryptedFile);

            // ファイルを復号
            DecryptFile(encryptedFile, decryptedFile, key);
            Console.WriteLine("復号完了: " + decryptedFile);

            // 復号したファイルの内容を表示
            string decryptedText = File.ReadAllText(decryptedFile);
            Console.WriteLine("復号した内容:");
            Console.WriteLine(decryptedText);
        }
        catch (Exception ex)
        {
            Console.WriteLine("エラーが発生しました: " + ex.Message);
        }
    }
}
暗号化完了: test.enc
復号完了: test_dec.txt
復号した内容:
これはAESで暗号化されたテキストのテストです。

鍵生成と保管

CSPRNGを利用した鍵生成

AESの鍵は安全性を確保するために、予測不可能な乱数で生成する必要があります。

単純な擬似乱数生成器ではなく、暗号学的に安全な乱数生成器(CSPRNG: Cryptographically Secure Pseudo-Random Number Generator)を使うことが必須です。

C#では標準でCSPRNGを提供しており、これを利用して安全な鍵を生成します。

RNGCryptoServiceProviderの活用例

RNGCryptoServiceProviderは.NET Frameworkから利用可能なCSPRNGの一つです。

以下は256ビット(32バイト)のAES鍵を生成する例です。

using System.Security.Cryptography;
public static byte[] GenerateKeyWithRNGCryptoServiceProvider()
{
    byte[] key = new byte[32]; // 256ビット鍵
    using (var rng = new RNGCryptoServiceProvider())
    {
        rng.GetBytes(key);
    }
    return key;
}

このコードはRNGCryptoServiceProviderGetBytesメソッドで安全な乱数を生成し、鍵として利用できるバイト配列を作成しています。

using構文でリソースを確実に解放している点もポイントです。

RandomNumberGenerator.GetBytesの使用例

.NET Core以降や.NET 5/6/7では、RandomNumberGeneratorクラスの静的メソッドGetBytesが推奨されています。

こちらの方がコードがシンプルで、同様に安全な乱数を生成できます。

using System.Security.Cryptography;
public static byte[] GenerateKeyWithRandomNumberGenerator()
{
    byte[] key = new byte[32]; // 256ビット鍵
    RandomNumberGenerator.Fill(key);
    return key;
}

RandomNumberGenerator.Fillは指定したバイト配列を安全な乱数で埋めるメソッドです。

RNGCryptoServiceProviderよりも簡潔に書けるため、最新の環境ではこちらを使うことが多いです。

鍵の永続化オプション

生成した鍵は暗号化・復号化の両方で使うため、適切に保存しなければなりません。

鍵の保管方法はセキュリティ要件や運用環境に応じて選択します。

ファイルシステム上での安全な保存

最も単純な方法は、鍵をファイルに保存することです。

ただし、平文で保存すると漏洩リスクが高いため、以下の対策が必要です。

  • 鍵ファイルのアクセス権限を厳格に設定し、許可されたユーザーのみが読み書きできるようにします
  • 鍵ファイル自体を暗号化して保存します。例えば、AESで別の安全な鍵を使って暗号化する方法があります
  • 鍵ファイルのパスワード保護やハードウェアセキュリティモジュール(HSM)を利用することも検討します

ファイルに保存する場合の例:

using System.IO;
public static void SaveKeyToFile(byte[] key, string filePath)
{
    File.WriteAllBytes(filePath, key);
    // 実運用ではファイルのアクセス権限設定を必ず行うこと
}

Windows DPAPIの併用

Windows環境では、Data Protection API(DPAPI)を使って鍵を安全に保護できます。

DPAPIはユーザーやマシンに紐づいた暗号化を提供し、鍵の管理を簡単にします。

鍵をDPAPIで暗号化して保存する例:

using System.Security.Cryptography;
using System.IO;
public static void SaveKeyWithDPAPI(byte[] key, string filePath)
{
    byte[] encryptedKey = ProtectedData.Protect(key, null, DataProtectionScope.CurrentUser);
    File.WriteAllBytes(filePath, encryptedKey);
}
public static byte[] LoadKeyWithDPAPI(string filePath)
{
    byte[] encryptedKey = File.ReadAllBytes(filePath);
    return ProtectedData.Unprotect(encryptedKey, null, DataProtectionScope.CurrentUser);
}

この方法なら、鍵ファイルが盗まれても、同じユーザーアカウントでなければ復号できません。

Windows専用ですが、手軽に鍵の保護が可能です。

Azure Key VaultやAWS KMSなどのクラウド保管

クラウド環境や大規模システムでは、鍵管理サービス(KMS: Key Management Service)を利用するのがベストプラクティスです。

代表的なサービスにAzure Key VaultやAWS KMSがあります。

  • Azure Key Vault

鍵の生成、保管、アクセス制御、監査ログをクラウド上で一元管理できます。

アプリケーションはAPI経由で鍵を取得し、暗号化処理に利用します。

  • AWS KMS

同様にAWSのクラウド環境で鍵管理を行い、暗号化・復号化の操作をAPIで安全に実行できます。

これらのサービスを使うメリットは以下の通りです。

  • 鍵の物理的な保護とアクセス制御が強固
  • 鍵のローテーションや監査が容易
  • アプリケーション側で鍵を直接扱わずに済むため漏洩リスクが低減

C#からはAzure SDKやAWS SDKを使って簡単に連携できます。

例えばAzure Key Vaultから鍵を取得するコード例は以下のようになります(SDKのセットアップが必要です)。

// Azure.Identity と Azure.Security.KeyVault.Keys パッケージが必要
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using System.Threading.Tasks;
public async Task<byte[]> GetKeyFromAzureKeyVaultAsync(string keyVaultUrl, string keyName)
{
    var client = new KeyClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
    var key = await client.GetKeyAsync(keyName);
    // キーの利用方法は用途に応じて異なるため、ここでは取得例のみ示す
    return key.Value.Key.ToByteArray();
}

クラウドKMSを利用する場合は、鍵の直接保存を避け、API経由で暗号化・復号化を行う設計も可能です。

これらの方法を組み合わせて、鍵の生成と保管を安全に行うことが、AES暗号化のセキュリティを高めるポイントです。

特に鍵の漏洩を防ぐために、単純なファイル保存だけでなくDPAPIやクラウドKMSの活用を検討してください。

コード実装の詳細

CryptoStreamによるデータ入出力

AES暗号化・復号化の実装では、CryptoStreamクラスを使ってストリーム上で暗号処理を行います。

CryptoStreamは、暗号化や復号化の変換器ICryptoTransformをラップし、読み書きの際に自動的にデータを変換します。

これにより、ファイルやメモリストリームなどの入出力をシームレスに暗号化・復号化できます。

FileStreamとの組み合わせ

ファイルの暗号化・復号化では、FileStreamCryptoStreamを組み合わせて使います。

具体的には、暗号化時はFileStreamに書き込む際にCryptoStreamを介し、復号化時はFileStreamから読み込む際にCryptoStreamを介します。

暗号化の例:

  • 入力ファイルをFileStreamで開く(読み込みモード)
  • 出力ファイルをFileStreamで開く(書き込みモード)
  • 出力ファイルのFileStreamCryptoStreamを作成し、暗号化用のICryptoTransformを渡す
  • 入力ファイルの内容をCryptoStreamにコピーすると、自動的に暗号化されて出力ファイルに書き込まれる

復号化の例は逆の流れで、CryptoStreamを読み込みモードで作成し、復号化用のICryptoTransformを渡します。

この組み合わせにより、ファイルの読み書きと暗号処理が一体化し、コードがシンプルになります。

バッファサイズの調整

FileStreamCryptoStreamのパフォーマンスはバッファサイズに影響されます。

デフォルトのバッファサイズは4KB程度ですが、大きなファイルを扱う場合はバッファサイズを大きくすることでI/O回数を減らし、処理速度を向上させられます。

例えば、64KB(65536バイト)程度のバッファを使うと効率的です。

using (FileStream fsIn = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 65536))
using (FileStream fsOut = new FileStream(outputFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 65536))
using (CryptoStream cs = new CryptoStream(fsOut, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
    fsIn.CopyTo(cs, 65536);
}

バッファサイズはCopyToメソッドの第2引数で指定でき、適切に調整することでメモリ使用量と速度のバランスを取れます。

エラーハンドリング戦略

暗号化処理では、ファイルアクセスや暗号化アルゴリズムの例外が発生する可能性があります。

特にCryptographicExceptionは暗号処理中に起こりやすいため、適切に対処することが重要です。

CryptographicExceptionの対処

CryptographicExceptionは、鍵やIVの不正、データ破損、パディングエラーなどで発生します。

復号時に多い例外で、例えば間違った鍵や破損したファイルを復号しようとした場合に起こります。

対処方法としては以下が挙げられます。

  • 例外をキャッチしてユーザーに復号失敗を通知する
  • ログに詳細なエラー情報を記録する(ただし機密情報は含めない)
  • 復号前にファイルの整合性チェックを行う(ハッシュや認証タグの検証)
try
{
    // 復号処理
}
catch (CryptographicException ex)
{
    Console.WriteLine("復号に失敗しました。鍵やファイルが正しいか確認してください。");
    // ログ出力など
}

例外安全なストリームクローズ

ストリームを使う処理では、例外が発生しても必ずリソースを解放することが重要です。

using文を使うことで、例外の有無にかかわらずストリームが確実に閉じられます。

using (FileStream fs = new FileStream(...))
using (CryptoStream cs = new CryptoStream(...))
{
    // 処理
}

もしusingを使わない場合は、try-finallyで明示的にDisposeCloseを呼び出す必要があります。

これによりファイルロックやメモリリークを防げます。

非同期I/Oへの書き換え

大容量ファイルの暗号化やUIスレッドをブロックしない処理には、非同期I/Oを活用すると効率的です。

C#のasync/awaitパターンを使うことで、簡潔に非同期処理を実装できます。

async/awaitパターン

FileStreamCryptoStreamは非同期メソッドをサポートしており、CopyToAsyncReadAsyncWriteAsyncを使えます。

以下は暗号化処理の非同期版の例です。

using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
public class AesFileEncryptorAsync
{
    public static async Task EncryptFileAsync(string inputFile, string outputFile, byte[] key)
    {
        using (Aes aes = Aes.Create())
        {
            aes.Key = key;
            aes.GenerateIV();
            using (FileStream fsOut = new FileStream(outputFile, FileMode.Create, FileAccess.Write, FileShare.None, 65536, useAsync: true))
            {
                await fsOut.WriteAsync(aes.IV, 0, aes.IV.Length);
                using (CryptoStream cs = new CryptoStream(fsOut, aes.CreateEncryptor(), CryptoStreamMode.Write))
                using (FileStream fsIn = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, 65536, useAsync: true))
                {
                    await fsIn.CopyToAsync(cs);
                }
            }
        }
    }
}

このコードはファイルの読み書きを非同期で行い、UIの応答性を保ちながら大容量ファイルの暗号化が可能です。

パフォーマンス比較

非同期I/Oは主に以下のメリットがあります。

  • UIアプリケーションでのフリーズ防止
  • サーバーアプリケーションでのスケーラビリティ向上
  • I/O待ち時間の有効活用

ただし、CPU負荷が高い暗号化処理自体は非同期化しても処理時間は変わりません。

暗号化はCPUバウンド、ファイル読み書きはI/Oバウンドの処理であるため、非同期化はI/O待ちの効率化に寄与します。

小さなファイルや高速ストレージでは非同期の恩恵が少ない場合もありますが、大容量ファイルやネットワークストレージを扱う場合は非同期I/Oを積極的に使うことをおすすめします。

IVと認証タグ

初期化ベクトルの役割

AESなどのブロック暗号は、同じ平文を同じ鍵で暗号化すると同じ暗号文が生成されてしまい、セキュリティ上の問題が生じます。

これを防ぐために使われるのが「初期化ベクトル(IV: Initialization Vector)」です。

IVは暗号化の最初のブロックにランダムな値を与えることで、同じ平文でも異なる暗号文を生成できるようにします。

これにより、暗号文のパターン解析やリプレイ攻撃を防止できます。

重要なポイントは以下の通りです。

  • IVは暗号化ごとにランダムに生成する必要がある
  • IVの長さはAESのブロックサイズ(128ビット=16バイト)に固定されている
  • IVは秘密にする必要はなく、暗号文と一緒に保存・送信して問題ない
  • 復号時には暗号化時と同じIVを使う必要があるため、IVを安全に伝達・保存することが重要

GCMモードでのタグ生成

AESのGCM(Galois/Counter Mode)は、暗号化と同時に認証タグ(Authentication Tag)を生成する認証付き暗号モードです。

認証タグはデータの完全性と認証を保証し、改ざんや不正なデータの検出に役立ちます。

GCMモードの特徴は以下の通りです。

  • 暗号化と認証を同時に行うため、別途MACを付ける必要がない
  • 認証タグは通常16バイト(128ビット)で、暗号文と一緒に保存される
  • 復号時に認証タグを検証し、改ざんがあれば復号処理を失敗させる

認証付き暗号のメリット

認証付き暗号(Authenticated Encryption)は、単にデータを暗号化するだけでなく、データの改ざん検知も行います。

これにより以下のメリットがあります。

  • 改ざん検知

データが途中で変更されていないかを検証できるため、改ざんや不正アクセスを防止できます。

  • 安全性の向上

従来のCBCモードなどでは改ざん検知が別途必要でしたが、GCMは一体化されているため安全性が高いでしょう。

  • 効率的な処理

認証タグの生成は高速で、パフォーマンスに大きな影響を与えにくい。

これらの理由から、GCMモードは近年の暗号化実装で推奨されるモードとなっています。

IVとタグのファイル先頭への付与方法

暗号化ファイルにIVや認証タグを付与する方法は、復号時に必要な情報を確実に取得できるように設計する必要があります。

一般的な方法は、IVと認証タグを暗号文の前後に付加し、ファイルの先頭にまとめて保存することです。

具体的な構成例は以下の通りです。

項目サイズ(バイト)説明
IV16AESの初期化ベクトル
暗号文可変実際に暗号化されたデータ
認証タグ(GCM)16認証付き暗号の検証用タグ

暗号化時の処理例:

  1. IVを生成し、ファイルの先頭に書き込む
  2. 暗号文を書き込む
  3. GCMの認証タグをファイルの末尾に書き込む(またはIVの後にまとめて書き込む場合もある)

復号時はファイルの先頭からIVを読み取り、暗号文を読み込み、最後に認証タグを取得して検証します。

C#の実装例では、AesGcmクラスを使う場合、認証タグは別途バッファに格納されるため、ファイルに書き込む際はIV、暗号文、タグの順に連結して保存します。

// IV、暗号文、タグを連結してファイルに保存するイメージ
// [IV (16バイト)] + [暗号文] + [タグ (16バイト)]

このようにファイルフォーマットを統一しておくことで、復号時に正確にIVとタグを取り出せ、改ざん検知も確実に行えます。

IVと認証タグの適切な管理は、AES暗号化の安全性を大きく左右します。

特にGCMモードを使う場合は、認証タグの保存と検証を忘れずに実装してください。

パフォーマンス最適化

バッファリングとメモリ効率

ファイルのAES暗号化・復号化において、バッファリングは処理速度とメモリ使用量のバランスを取る重要なポイントです。

バッファサイズが小さすぎるとI/O操作が頻繁になり、オーバーヘッドが増加します。

一方、大きすぎるとメモリ消費が増え、システム全体のパフォーマンスに悪影響を及ぼす可能性があります。

C#のFileStreamCryptoStreamでは、バッファサイズを指定してストリームを作成できます。

一般的には64KB(65536バイト)程度のバッファサイズが推奨されますが、環境やファイルサイズに応じて調整が必要です。

バッファサイズの調整例:

  • 小さなファイル(数MB以下):デフォルトの4KB〜8KBでも十分
  • 中〜大容量ファイル(数十MB〜数GB):64KB〜256KB程度に設定するとI/O回数が減り高速化が期待できる

バッファリングの効果を最大限に活かすためには、CopyToCopyToAsyncの第2引数でバッファサイズを指定し、FileStreamのコンストラクタでもバッファサイズを設定します。

using (FileStream fsIn = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 65536))
using (FileStream fsOut = new FileStream(outputFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 65536))
using (CryptoStream cs = new CryptoStream(fsOut, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
    fsIn.CopyTo(cs, 65536);
}

このようにバッファサイズを統一することで、メモリ効率と処理速度のバランスを最適化できます。

大容量ファイルの分割暗号化

非常に大きなファイル(数GB以上)を一括で暗号化すると、メモリ使用量や処理時間が増大し、システムに負荷がかかります。

こうした場合はファイルを分割して暗号化・復号化する方法が有効です。

分割暗号化のポイントは以下の通りです。

  • ファイルを一定サイズ(例:100MBや500MB)ごとに分割して処理する
  • 各分割ファイルごとに独立したIVを生成し、暗号化する
  • 鍵は共通のものを使い、分割ファイルごとに復号可能にする
  • 分割ファイルの管理(ファイル名や順序)を明確にしておく

分割暗号化のメリットは、メモリ使用量を抑えられることと、並列処理や部分的な復号が可能になることです。

分割ファイルの例:

ファイル名内容
data.part1.aesファイルの最初の100MB暗号化データ
data.part2.aes次の100MB暗号化データ

分割ファイルごとにIVを先頭に付与し、復号時に正しく読み取る設計にします。

マルチスレッド処理の検討

パフォーマンス向上のために、マルチスレッドや並列処理を活用する方法もあります。

特に複数のファイルを同時に暗号化・復号化する場合や、分割ファイルを並列処理する場合に効果的です。

マルチスレッド処理のポイントは以下の通りです。

  • 分割ファイルごとに独立したスレッドやタスクで暗号化・復号化を行う
  • CPUコア数やI/O帯域を考慮して同時実行数を制御する
  • スレッド間で鍵やリソースの競合が起きないように設計する

C#ではTaskParallel.ForEachを使って簡単に並列処理が可能です。

using System.Threading.Tasks;
public static void EncryptFilesInParallel(string[] inputFiles, string outputDir, byte[] key)
{
    Parallel.ForEach(inputFiles, inputFile =>
    {
        string outputFile = Path.Combine(outputDir, Path.GetFileName(inputFile) + ".aes");
        AesFileEncryptor.EncryptFile(inputFile, outputFile, key);
    });
}

ただし、暗号化はCPU負荷が高いため、過度な並列化は逆にパフォーマンスを低下させることがあります。

適切なスレッド数の調整やI/O待ち時間のバランスを考慮してください。

これらのパフォーマンス最適化手法を組み合わせることで、効率的かつ安全にAESによるファイル暗号化・復号化を実現できます。

特に大容量ファイルや大量ファイルを扱う場合は、バッファリング、分割処理、並列化の検討が重要です。

セキュリティチェックリスト

鍵交換時に避けるべきパターン

AESのような共通鍵暗号方式では、暗号化と復号化に同じ鍵を使うため、鍵の安全な交換が非常に重要です。

鍵交換時に避けるべき典型的なパターンを理解し、セキュリティリスクを減らしましょう。

  • 平文での鍵送信

ネットワークやメールなどで鍵を暗号化せずに送信することは絶対に避けてください。

第三者に盗聴されるリスクが高く、鍵が漏洩すると暗号化の意味がなくなります。

  • 弱い鍵交換プロトコルの使用

鍵交換に安全でないプロトコル(例:単純なHTTP通信や古い暗号化方式)を使うのは危険です。

TLS(Transport Layer Security)などの安全な通信路を利用してください。

  • 鍵の再利用

同じ鍵を長期間使い続けたり、複数の通信やファイルで使い回すことは避けましょう。

鍵の寿命を短くし、定期的に新しい鍵に更新することが推奨されます。

  • 鍵の共有範囲が広すぎる

鍵を必要以上に多くの人やシステムに共有すると、漏洩リスクが増大します。

最小限の範囲で鍵を共有し、アクセス制御を厳格に行いましょう。

  • 鍵の保存場所が不適切

鍵を安全でない場所(例:ソースコード、共有フォルダ、メールボックス)に保存することは避けてください。

安全な保管方法を必ず採用しましょう。

ソースコードに鍵をハードコードしない

開発時に鍵をソースコードに直接書き込む(ハードコード)することは、セキュリティ上の大きな問題です。

以下の理由から絶対に避けてください。

  • リポジトリの漏洩リスク

ソースコード管理システム(Gitなど)に鍵が含まれると、誰でもアクセス可能になる可能性があります。

  • バイナリ解析で鍵が露出

コンパイル後の実行ファイルを逆アセンブルやデコンパイルすると、鍵が簡単に見つかることがあります。

  • 鍵の更新が困難

鍵をコードに埋め込むと、鍵を変更するたびにコードの修正と再ビルドが必要になり、運用が煩雑になります。

代わりに以下の方法を使いましょう。

  • 環境変数や設定ファイルから読み込む

鍵を外部の安全な設定ファイルや環境変数に保存し、実行時に読み込む方法です。

設定ファイルはアクセス権限を厳しく管理します。

  • 安全な鍵管理サービスを利用する

Azure Key VaultやAWS KMSなどのクラウド鍵管理サービスを使い、アプリケーションはAPI経由で鍵を取得します。

  • DPAPIなどOSの保護機能を利用する

WindowsのDPAPIを使って鍵を暗号化して保存し、実行時に復号して利用します。

アップデートとアルゴリズム寿命

暗号アルゴリズムや鍵の寿命は有限であり、セキュリティを維持するためには定期的なアップデートが必要です。

  • アルゴリズムの脆弱性に注意

AESは現在も安全とされていますが、将来的に新たな攻撃手法が発見される可能性があります。

最新のセキュリティ情報を常にチェックし、必要に応じてアルゴリズムの見直しを行いましょう。

  • 鍵のローテーション

鍵は一定期間ごとに新しいものに更新することが推奨されます。

鍵の寿命は利用環境やセキュリティポリシーによりますが、半年から1年程度が一般的です。

  • ソフトウェアのアップデート

暗号ライブラリやフレームワークのアップデートを怠らず、脆弱性修正や性能改善を取り入れましょう。

  • 互換性の確保

アップデート時は、旧バージョンの暗号化データの復号が可能かどうかを検証し、移行計画を立てることが重要です。

これらを踏まえ、セキュリティを維持しつつ安全なファイル暗号化システムを運用してください。

単体テストと自動化

テスト用ダミーファイルの生成

暗号化・復号処理の単体テストでは、実際のファイルを使って動作確認を行うことが重要です。

テスト用のダミーファイルは、テストの再現性と効率を高めるために自動生成するのが望ましいです。

ダミーファイルの生成方法としては、ランダムなバイト列をファイルに書き込む方法が一般的です。

ファイルサイズは小さなものから大きなものまで複数用意し、様々なケースをカバーします。

以下はC#でランダムなバイト列を生成し、指定したサイズのファイルを作成する例です。

using System;
using System.IO;
using System.Security.Cryptography;
public static class TestFileGenerator
{
    public static void CreateRandomFile(string filePath, int sizeInBytes)
    {
        byte[] data = new byte[sizeInBytes];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(data);
        }
        File.WriteAllBytes(filePath, data);
    }
}

このメソッドを使うと、例えば1MBのダミーファイルを簡単に作成できます。

テストの前に毎回ファイルを生成することで、テストの独立性と信頼性が向上します。

Assertで暗号化・復号の一致確認

単体テストの目的は、暗号化したデータを復号した結果が元のデータと完全に一致することを検証することです。

これには、Assertを使ってファイルの内容を比較します。

テストの流れは以下の通りです。

  1. ダミーファイルを生成する
  2. 生成したファイルを暗号化する
  3. 暗号化ファイルを復号化する
  4. 復号化したファイルの内容と元のダミーファイルの内容を比較する

ファイルの内容比較は、バイト単位で行い、一致しなければテスト失敗とします。

以下はNUnitを使ったテスト例です。

using NUnit.Framework;
using System.IO;
[TestFixture]
public class AesEncryptionTests
{
    private const string OriginalFile = "test_original.bin";
    private const string EncryptedFile = "test_encrypted.bin";
    private const string DecryptedFile = "test_decrypted.bin";
    private byte[] key;
    [SetUp]
    public void Setup()
    {
        key = new byte[32];
        using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
        {
            rng.GetBytes(key);
        }
        TestFileGenerator.CreateRandomFile(OriginalFile, 1024 * 1024); // 1MBファイル生成
    }
    [Test]
    public void EncryptAndDecrypt_ShouldReturnOriginalData()
    {
        AesFileEncryptor.EncryptFile(OriginalFile, EncryptedFile, key);
        AesFileDecryptor.DecryptFile(EncryptedFile, DecryptedFile, key);
        byte[] originalData = File.ReadAllBytes(OriginalFile);
        byte[] decryptedData = File.ReadAllBytes(DecryptedFile);
        Assert.AreEqual(originalData.Length, decryptedData.Length, "ファイルサイズが一致しません。");
        Assert.AreEqual(originalData, decryptedData, "復号データが元データと一致しません。");
    }
    [TearDown]
    public void Cleanup()
    {
        if (File.Exists(OriginalFile)) File.Delete(OriginalFile);
        if (File.Exists(EncryptedFile)) File.Delete(EncryptedFile);
        if (File.Exists(DecryptedFile)) File.Delete(DecryptedFile);
    }
}

このテストでは、暗号化・復号化の正確性を自動的に検証できます。

Assert.AreEqualでバイト配列の一致を確認し、問題があればテストが失敗します。

CI環境への組み込み

単体テストは継続的インテグレーション(CI)環境に組み込むことで、コードの変更が暗号化機能に影響を与えていないか自動的に検証できます。

CI環境に組み込む際のポイントは以下の通りです。

  • テストの自動実行

プッシュやプルリクエスト時に自動でテストが走るように設定します。

GitHub Actions、Azure DevOps、JenkinsなどのCIツールが利用可能です。

  • テスト用ファイルの管理

テスト用ファイルはテストコード内で生成・削除し、環境に依存しないようにします。

これによりCI環境でも安定して動作します。

  • ログとレポートの活用

テスト結果のログやレポートをCIツールで確認し、失敗時に原因を特定しやすくします。

  • パフォーマンステストの追加

必要に応じて暗号化・復号化の処理時間を計測し、パフォーマンスの劣化を検知するテストも組み込みます。

GitHub Actionsの簡単な例:

name: .NET Build and Test
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:

    - uses: actions/checkout@v3
    - name: Setup .NET

      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '7.0.x'

    - name: Restore dependencies

      run: dotnet restore

    - name: Build

      run: dotnet build --no-restore --configuration Release

    - name: Test

      run: dotnet test --no-build --verbosity normal

このようにCI環境にテストを組み込むことで、品質を継続的に保ちながら安全なAES暗号化機能を運用できます。

デプロイ後の運用

ロギングにおける機密情報マスキング

運用中のシステムではログが重要な役割を果たしますが、暗号化処理に関わるログには機密情報が含まれる可能性があるため、適切なマスキングが必要です。

鍵やパスフレーズ、IV、認証タグなどの機密データをそのままログに出力すると、情報漏洩のリスクが高まります。

機密情報マスキングのポイントは以下の通りです。

  • 鍵やパスワードは絶対にログに出さない

例外的にデバッグ目的で一部を表示する場合も、全体を隠すかハッシュ化して表示します。

  • IVや認証タグは必要に応じて部分的にマスク

これらは復号に必要な情報ですが、ログに出す場合は全体を表示せず、先頭数バイトだけを表示するなどの工夫をします。

  • ログレベルの設定を活用

通常は機密情報を含む詳細ログは出さず、トラブルシューティング時のみ限定的に有効にします。

  • ログフォーマットの統一と自動マスキング

ログ出力時に自動的に機密情報を検出してマスクする仕組みを導入すると安全性が向上します。

例として、鍵の一部をマスクしてログに出すコード例を示します。

string MaskKey(byte[] key)
{
    if (key == null || key.Length < 4) return "****";
    return BitConverter.ToString(key, 0, 2) + "-**-**";
}

このように、ログに機密情報が含まれないように注意しながら運用してください。

監査ログの保管期間

監査ログはシステムのセキュリティや運用状況を記録する重要な資料です。

暗号化処理に関するログも含め、適切な保管期間を設定し、必要に応じてログの保全や削除を行うことが求められます。

  • 保管期間の決定

法令や業界標準、社内ポリシーに基づき、ログの保管期間を設定します。

一般的には6ヶ月から数年の範囲で設定されることが多いです。

  • ログの改ざん防止

ログは改ざんされないように保管し、必要に応じてデジタル署名やハッシュを付与して整合性を保証します。

  • アクセス制御

監査ログへのアクセスは厳格に制限し、閲覧や操作の履歴も記録します。

  • ログのアーカイブと削除

保管期間終了後は安全にログを削除するか、長期保存が必要な場合はアーカイブして保管します。

これらの管理を徹底することで、セキュリティインシデント発生時の調査やコンプライアンス対応がスムーズになります。

障害発生時の復旧フロー

暗号化システムに障害が発生した場合、迅速かつ確実に復旧するためのフローを整備しておくことが重要です。

特に鍵の紛失や破損、ファイルの破損などは重大な問題となるため、事前の準備と手順の明確化が求められます。

復旧フローの主なステップは以下の通りです。

  1. 障害の検知と初期対応

ログや監視ツールで障害を検知し、影響範囲を特定。

必要に応じてシステムを一時停止し、被害拡大を防止。

  1. 原因の特定

鍵の管理状況、ファイルの整合性、システム設定などを確認し、障害の根本原因を調査。

  1. バックアップからの復元

鍵や暗号化ファイルのバックアップがあれば、最新の正常な状態に復元。

バックアップは定期的に取得し、安全に保管しておくことが必須。

  1. 鍵の再発行・ローテーション

鍵が紛失または漏洩した場合は速やかに新しい鍵を発行し、影響範囲のデータを再暗号化するなどの対応を行います。

  1. 復旧後の検証

復旧処理が完了したら、暗号化・復号化の動作確認やデータ整合性チェックを実施し、正常に復旧したことを確認。

  1. 再発防止策の実施

障害原因に基づき、運用ルールの見直しやシステム強化を行い、同様の障害が起きないよう対策を講じます。

これらの手順を文書化し、関係者に周知徹底することで、障害発生時の混乱を最小限に抑えられます。

特に鍵管理のバックアップと復旧手順は、暗号化システムの信頼性を支える重要な要素です。

小さなファイルでのオーバーヘッドは?

AES暗号化を小さなファイルに適用すると、暗号化処理に伴うオーバーヘッドが相対的に大きく感じられることがあります。

これは主に以下の要因によります。

  • 初期化ベクトル(IV)や認証タグのサイズ

暗号化ファイルにはIV(通常16バイト)やGCMモードの場合は認証タグ(通常16バイト)が付加されます。

小さなファイルではこれらの付加情報がファイルサイズに対して大きな割合を占めるため、ファイルサイズが増加します。

  • パディングの影響

AESはブロック単位(128ビット=16バイト)で処理するため、ファイルサイズがブロックサイズの倍数でない場合はパディングが追加されます。

小さいファイルほどパディングの割合が大きくなります。

  • 処理時間の固定コスト

暗号化処理には初期化や鍵設定などの固定的な処理時間がかかるため、ファイルサイズが小さいと相対的に処理時間が長く感じられます。

これらの理由から、小さなファイルの暗号化ではオーバーヘッドが目立ちますが、セキュリティ上は問題ありません。

もし小さなファイルを大量に扱う場合は、複数ファイルをまとめて暗号化するか、別の軽量な暗号化方式を検討することもあります。

同じ鍵を複数ファイルで共有しても安全?

同じAES鍵を複数のファイルで使うことは技術的には可能ですが、セキュリティ上のリスクを考慮する必要があります。

  • IVの使い回しを避ける

同じ鍵を使う場合でも、各ファイルごとに異なるランダムなIVを必ず生成し使用することが必須です。

IVが重複すると暗号文のパターンが解析されやすくなり、暗号の安全性が大きく低下します。

  • 鍵の管理リスク

複数ファイルで同じ鍵を使うと、鍵が漏洩した場合に影響範囲が広がります。

重要度の異なるファイルを同じ鍵で暗号化するのは避け、用途や重要度に応じて鍵を分けることが望ましいです。

  • 鍵のローテーション

定期的に鍵を更新し、新しい鍵で暗号化し直す運用を行うことでリスクを軽減できます。

まとめると、同じ鍵を複数ファイルで共有する場合は、必ず異なるIVを使い、鍵の管理とローテーションを厳格に行うことが安全性確保のポイントです。

暗号化ファイルの拡張子は変えるべき?

暗号化ファイルの拡張子を変更するかどうかは運用や管理の観点から検討されます。

  • 拡張子を変えるメリット
    • 暗号化ファイルであることが一目でわかるため、誤って平文として扱うリスクを減らせます
    • ファイル管理やバックアップ時に暗号化ファイルを識別しやすくなります
    • セキュリティポリシーで暗号化ファイルの扱いを区別しやすい
  • 拡張子を変えない場合の注意点
    • 元の拡張子のままだと、ユーザーやシステムが誤ってファイルを開こうとする可能性があります
    • ファイルの内容が暗号化されているため、通常のアプリケーションでは開けずエラーになることがあります

一般的には、.aes.encなどの専用拡張子を付けることが多いです。

これにより、暗号化ファイルであることを明示でき、運用上の混乱を防げます。

ただし、拡張子の変更は必須ではなく、システム要件や運用ルールに応じて柔軟に決めて問題ありません。

重要なのは、暗号化ファイルの取り扱いを明確にし、誤操作を防止することです。

まとめ

この記事では、C#でAESを使った安全なファイル暗号化と復号の実装手順を詳しく解説しました。

鍵とIVの生成からファイルの読み書き、認証付き暗号の扱い、パフォーマンス最適化、鍵管理のコツまで幅広くカバーしています。

さらに、エラーハンドリングや非同期処理、運用時の注意点、テスト自動化の方法も紹介しました。

これにより、安全かつ効率的なAES暗号化システムの構築と運用が可能になります。

関連記事

Back to top button