.NET Framework C# で暗号化する AES(Advanced Encryption Standard)

暗号化をする必要がありましたので、調べたことをまとめてみます。

環境

  • Windows 10
  • Visual Studio 2013
  • .NET Framework 4.5.2(C#, VB.NET)

暗号化の基礎

暗号化についての基礎知識はこのあたりを参考にしました。

対称暗号化と非対称暗号化

対称暗号化と非対称暗号化は、異なるプロセスを使用して実行されます。 対称暗号化は、ストリーム上で実行されるため、大量のデータの暗号化に役立ちます。 非対称暗号化は、少ないバイト数で実行されるため、少量のデータにのみ役立ちます。

データの暗号化

大量のデータを暗号化するには対称暗号化が良いそうです。

アルゴリズム

.NET Framework でアルゴリズムを実装する方法

アルゴリズムに使用できるさまざまな実装例として、対称アルゴリズムを検討します。 すべての対称アルゴリズムのベースは SymmetricAlgorithm であり、次のアルゴリズムによって継承されます。

  1. Aes
  2. DES
  3. RC2
  4. Rijndael
  5. TripleDES

.NET Framework の暗号モデル

.NET Framework では対称アルゴリズムが 5 種類あるようです。

DES

「DES(Data Encryption Standard)」は、1977年に米国商務省標準局(NBS:National Bureau of Standard。現NIST:National Institute of Standards and Technology)によって制定された、標準のデータ暗号化規格である。

…略…

DESでは64bitのデータブロックと56bitのキーを使っている。だが、規格制定当初からこの程度の安全性では(将来のコンピューターの進歩などを考えると)不十分ではないかとも言われていた。

その後、その懸念は現実のものとなり、現在では総当たり攻撃でも容易に解読できてしまうようになってしまった。そのためDESは標準規格としては非推奨となり、現在では後継のAESなどの暗号化技術の使用が推奨されている。

…略…

上の二重DESをさらに拡張して、DES処理を3回行う「3 DES(トリプルDES)」という方式が考えられ、いくつかのケースで利用されている。

ただしこの方法を使っても、元はDESなのであまり安全性が高いとは言えない。IPA(独立行政法人 情報処理推進機構)では、トリプルDESの使用を2030年までにするように勧告している。

「SSL/TLS暗号設定ガイドライン~安全なウェブサイトのために(暗号設定対策編)(独立行政法人 情報処理推進機構)

マスターIT/暗号技術:第2回 DES暗号化 - @IT

DES、トリプル DES は推奨されていないようです。

AES

「AES(Advanced Encryption Standard)」は、DESの後継として米国の国立標準技術研究所(NIST:National Institute of Standards and Technology)によって制定された新しい暗号化規格である。

…略…

ある暗号化アルゴリズムが安全かどうかは、それに対する攻撃法(解読方法)があるかどうかや、もしなければ、総当たり攻撃でどのくらいの計算量で解読できるか、などで評価される。

AESの提唱後、さまざまな解読方法やアルゴリズム上の脆弱(ぜいじゃく)性などが研究されているが、いまのところ、誰でもすぐに実行できるような簡単な解読方法は発見されていない。

マスターIT/暗号技術:第3回 AES暗号化 - @IT

AES は引用した記事の時点(2015/6)と、調べた感じではこの記事の時点(2017/1)でも、簡単には解読することはできなく、比較的安全そうです。

DESと違って、Rijndaelは鍵長やブロック長が可変の共通鍵方式のブロック暗号である。 提案されたRijndaelではいくつかの鍵長やブロック長が選べたが、最終的には次のようなパラメーターだけを使うことになり、これが正式なAES規格となった。

マスターIT/暗号技術:第3回 AES暗号化 - @IT

Rijndael の鍵長やブロック長を不変にしたものが AES のようです。 だから、Rijndael の概念の一部が AES になると認識しました。 アメリカ国立標準技術研究所(NIST)が制定したものが AES なら、あえて Rijndael を採用することもないかと思いまして、AES を採用することにしました。

AES の .NET Framework の実装

Aes は、AesCryptoServiceProvider と AesManaged の 2 つのクラスによって継承されます。 AesCryptoServiceProvider クラスは Aes の Windows 暗号化 API (CAPI) 実装のラッパーですが、AesManaged クラスは全体がマネージ コードで書かれています。 さらに、マネージ実装と CAPI 実装に加え、3 つ目の実装、Cryptography Next Generation (CNG) もあります。 CNG アルゴリズムの例が ECDiffieHellmanCng です。 CNG アルゴリズムは、Windows Vista 以降のバージョンで利用可能です。

ご自身にとって最適な実装を選択できます。 マネージ実装は、.NET Framework をサポートするすべてのプラットフォームで利用できます。 CAPI 実装は、以前のオペレーティング システムで使用可能ですが、開発中止となっています。 CNG はまさに最新の実装であり、新しい開発が行われます。 ただし、マネージ実装は連邦情報処理規格 (FIPS: Federal Information Processing Standard) に認定されておらず、ラッパー クラスよりも低速である場合があります。

.NET Framework の暗号モデル

AES の実装クラスは 3 種類あるようです。 AesCryptoServiceProvider はCAPIですし、開発中止が気になります。 AesManaged は「連邦情報処理規格 (FIPS: Federal Information Processing Standard) に認定されておらず、ラッパー クラスよりも低速である」ところが気になります。 3 つ目の実装、Cryptography Next Generation (CNG) は検索したところ、情報があまり出てこないところが気になりました。 それぞれのデメリットを考慮して、今回は、比較的無難そうな AesManaged を採用することにしました。

運用

暗号化は鍵が必要になりますので、鍵の管理を含めた運用を設計しなければなりません。

共有キー暗号方式の弱点は、両者のキーと IV を一致させ、それぞれの値を転送しておく必要がある点です。 IV は秘密情報とは見なされないため、平文のメッセージで転送できます。 しかし、キーは承認されていないユーザーから保護する必要があります。 このような問題のため、共有キー暗号方式は公開キー暗号方式と併用されることがよくあります。 公開キー暗号方式は、キーと IV の値を秘密に通信するために使用されます。

暗号サービス

.

キーの作成と管理は、暗号プロセスの重要な部分です。 対称アルゴリズムでは、キーと初期化ベクター (IV) を作成する必要があります。 キーは、データの暗号化解除を許可しないユーザーに対しては秘密にする必要があります。 IV は秘密にする必要はありませんが、セッションごとに変更する必要があります。 非対称アルゴリズムでは、公開キーと秘密キーを作成する必要があります。 公開キーはだれに公開してもかまいせんが、秘密キーを知らせる相手は、公開キーで暗号化されたデータを復号化する人だけにします。 このセクションでは、対称アルゴリズムと非対称アルゴリズムの両方について、キーを作成して管理する方法を説明します。

暗号化と復号化のためのキーの生成

対称暗号化(共有キー暗号方式)では暗号化と復号化に同じ鍵を使う必要があります。 そして、一般的にその鍵は暗号化をするたびに毎回生成する必要があります。 そのため、その生成した鍵(鍵自体、鍵そのもの)を非対称暗号化(公開キー暗号方式)で暗号化して、別に転送するような運用が一般的のようです。

この前提として、非対称暗号化(公開キー暗号方式)の鍵は、転送元の利用者が転送先の利用者の公開鍵を持っている必要がある、事前に何らかの方法で受け渡しをしておく必要があることになります。

このあたり、対称暗号化(共有キー暗号方式)を採用したのに、非対称暗号化(公開キー暗号方式)のことも知っておかなければいけないので大変です。

C# での実装

アプリケーションで使用したかったので、StreamWriter クラス (System.IO), StreamReader クラス (System.IO)のようにテキストファイルを読み書きするのと同じ感じで使えるものにしました。

    /// <summary>
    /// 暗号化
    /// </summary>
    class Class1 : IDisposable
    {
        /// <summary>
        /// ブロック サイズ (ビット単位)。
        /// </summary>
        private const int BlockSize = 128;

        /// <summary>
        /// 対称アルゴリズムで使用されるキーのビット単位のサイズ。
        /// </summary>
        private const int KeySize = 128;

        /// <summary>
        /// 高度暗号化標準 (AES) の対称アルゴリズムのマネージ実装を提供します。
        /// </summary>
        private AesManaged aes;

        /// <summary>
        /// 同期および非同期の読み取り操作と書き込み操作をサポートするファイル用の Stream を提供します。
        /// </summary>
        private FileStream fs;

        /// <summary>
        /// データ ストリームを暗号変換にリンクするストリームを定義します。
        /// </summary>
        private CryptoStream cs;

        /// <summary>
        /// 新しいインスタンスを初期化、 StreamWriter 既定のエンコーディングを使用して、指定したファイルのクラスおよびバッファー サイズ。
        /// </summary>
        /// <param name="path">書き込むファイルの完全なパス。 path ファイル名にすることができます。</param>
        public Class1(string path)
        {
            // 高度暗号化標準(AES)の対象アルゴリズムのマネージ実装クラスの新しいインスタンスを初期化します。
            aes = new AesManaged();
            aes.BlockSize = BlockSize;
            aes.KeySize = KeySize;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;

            // キーと初期化ベクターを生成します。
            aes.GenerateKey();
            aes.GenerateIV();

            fs = new FileStream(path, FileMode.Create, FileAccess.Write);
            cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write);

            // ファイルの最初に初期化ベクターを出力します。
            // 初期化ベクターはCryptoStreamを通しません。
            fs.Write(aes.IV, 0, aes.IV.Length);
        }

        /// <summary>
        /// 対称アルゴリズムのキーです。
        /// </summary>
        /// <returns></returns>
        public byte[] Key()
        {
            return aes.Key;
        }

        /// <summary>
        /// 対称アルゴリズムで使用する初期化ベクター
        /// </summary>
        /// <returns></returns>
        public byte[] IV()
        {
            return aes.IV;
        }

        /// <summary>
        /// ストリームに文字列を書き込みます。
        /// </summary>
        /// <param name="value">ストリームに書き込む文字列。 場合 value が null の場合、何も書き込まれません。</param>
        public void WriteLine(string value)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(value);
            cs.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// 文字列をテキスト文字列またはストリームに書き込み、続けて行終端記号を書き込みます。
        /// </summary>
        /// <param name="value">書き込む文字列。 value が null の場合は、行終端記号だけを書き込みます。</param>
        public void WriteLine(string value)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(value + Environment.NewLine);
            cs.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// アンマネージ リソースの解放またはリセットに関連付けられているアプリケーション定義のタスクを実行します。
        /// </summary>
        void IDisposable.Dispose()
        {
            if (cs != null)
            {
                try { cs.Close(); }
                catch { }
            }
            if (fs != null)
            {
                try { fs.Close(); }
                catch { }
            }
        }
    }
    /// <summary>
    /// 復号化
    /// </summary>
    class Class2 : IDisposable
    {
        /// <summary>
        /// ブロック サイズ (ビット単位)。
        /// </summary>
        private const int BlockSize = 128;

        /// <summary>
        /// 対称アルゴリズムで使用されるキーのビット単位のサイズ。
        /// </summary>
        private const int KeySize = 128;

        /// <summary>
        /// 高度暗号化標準 (AES) の対称アルゴリズムのマネージ実装を提供します。
        /// </summary>
        private AesManaged aes;

        /// <summary>
        /// 同期および非同期の読み取り操作と書き込み操作をサポートするファイル用の Stream を提供します。
        /// </summary>
        private FileStream fs;

        /// <summary>
        /// データ ストリームを暗号変換にリンクするストリームを定義します。
        /// </summary>
        private CryptoStream cs;

        /// <summary>
        /// 特定のエンコーディングのバイト ストリームから文字を読み込む TextReader を実装します。
        /// </summary>
        private StreamReader sr;

        /// <summary>
        /// 指定したファイル名用の Class2 クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="path">読み込まれる完全なファイル パス。</param>
        /// <param name="key">対称アルゴリズムのキーです。</param>
        public Class2(string path, byte[] key)
        {
            // 高度暗号化標準(AES)の対象アルゴリズムのマネージ実装クラスの新しいインスタンスを初期化します。
            aes = new AesManaged();
            aes.BlockSize = BlockSize;
            aes.KeySize = KeySize;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;

            fs = new FileStream(path, FileMode.Open, FileAccess.Read);

            // ファイルの最初の16バイトから初期化ベクターを読み込みます。
            byte[] iv = new byte[16];
            fs.Read(iv, 0, iv.Length);

            cs = new CryptoStream(fs, aes.CreateDecryptor(key, iv), CryptoStreamMode.Read);
            sr = new StreamReader(cs);
        }

        /// <summary>
        /// 使用可能な次の文字を返しますが、その文字は使用されません。
        /// </summary>
        /// <returns>読み取り対象の次の文字を表す整数。読み取り対象の文字が存在しない場合またはストリームがシークをサポートしていない場合は -1。</returns>
        public int Peek()
        {
            return sr.Peek();
        }

        /// <summary>
        /// 入力ストリームから次の文字を読み込み、1 文字分だけ文字位置を進めます。
        /// </summary>
        /// <returns>Int32 オブジェクトで表した入力ストリームの次の文字。使用できる文字がない場合は -1。</returns>
        public int Read()
        {
            return sr.Read();
        }

        /// <summary>
        /// 現在のストリームから 1 行分の文字を読み取り、そのデータを文字列として返します。
        /// </summary>
        /// <returns>入力ストリームからの次の行。入力ストリームの末尾に到達した場合は null。</returns>
        public string ReadLine()
        {
            return sr.ReadLine();
        }

        /// <summary>
        /// アンマネージ リソースの解放またはリセットに関連付けられているアプリケーション定義のタスクを実行します。
        /// </summary>
        void IDisposable.Dispose()
        {
            if (sr != null)
            {
                try { sr.Close(); }
                catch { }
            }
            if (cs != null)
            {
                try { cs.Close(); }
                catch { }
            }
            if (fs != null)
            {
                try { fs.Close(); }
                catch { }
            }
        }
    }

AesManagedAesCryptoServiceProvider とほぼ同じ感じで使えるようでした。 Aes クラス (System.Security.Cryptography) という抽象基底クラスの変数で受け取って、コンストラクタの呼び出しを AesCryptoServiceProvider にするだけで入れ替えられそうです。

IV は秘密情報とは見なされないため、平文のメッセージで転送できます。

暗号サービス

IV は平文で良いそうですので、ファイルの先頭に含めています。

    /// <summary>
    /// 暗号化と復号化のテスト
    /// </summary>
    class Class3
    {
        static void Main(string[] args)
        {
            const string path1 = @".\encrypto.txt";
            const string path2 = @".\decrypto.txt";
            // キー
            byte[] key;
            // 暗号化
            using (Class1 c1 = new Class1(path1))
            {
                c1.WriteLine("暗号化テスト1");
                c1.WriteLine("暗号化テスト2");
                c1.WriteLine("暗号化テスト3");
                // 暗号化のキーを保存する
                key = c1.Key();
            }
            // 復号化
            using (Class2 c2 = new Class2(path1, key))
            using (StreamWriter sw = new StreamWriter(path2))
            {
                while (c2.Peek() >= 0)
                {
                    sw.WriteLine(c2.ReadLine());
                }
            }
        }
    }

実際は復号化するのは別の環境になるはずなので、暗号化したキーは byte[] ではなく、System.Convert.ToBase64String で文字列にして扱うことを想定しています。

終わり

暗号化って、ただ暗号化するだけでしょ?みたいな印象がありましたが、セキュリティーのことですので、時代と共に変わるでしょうし、検索した時にどれが最新の情報であるか、に注意してみました。

今のところまだ AES は比較的大丈夫そうな印象を持ちましたが、そう判断するまでに調べる時間はやっぱりかかるものでした。

参考