次の方法で共有


パディングを使用した CBC モード対称暗号化解除のタイミングの脆弱性

Microsoft は、検証可能な埋め込みが適用されている場合、暗号テキストの整合性を最初に確認せずに、対称暗号化の暗号Block-Chaining (CBC) モードで暗号化されたデータを暗号化解除することは、非常に具体的な状況を除いて安全ではなくなったと考えています。 この判断は、現在知られている暗号研究に基づいています。

イントロダクション

パディング オラクル攻撃は、暗号化されたデータに対する一種の攻撃であり、攻撃者はキーを知らずにデータの内容を復号化できます。

Oracle とは、実行しているアクションが正しいかどうかに関する情報を攻撃者に提供する "通知" を指します。 子供と一緒にボードやカードゲームをプレイすることを想像してみてください。 彼らが良い動きをしようとしていると思うので、彼らの顔が大きな笑顔で点灯するとき、それはオラクルです。 相手として、このオラクルを使用して、次の動きを適切に計画することができます。

パディングは、特定の暗号化用語です。 一部の暗号は、データの暗号化に使用されるアルゴリズムであり、各ブロックが固定サイズのデータ ブロックで動作します。 暗号化するデータがブロックを埋めるのに適したサイズでない場合は、データが埋め込まれるまでデータが埋め込まれます。 多くの形式のパディングでは、元の入力が適切なサイズであった場合でも、パディングを常に存在させる必要があります。 これにより、復号化時にパディングを常に安全に削除できます。

2 つのことをまとめると、パディング Oracle を使用したソフトウェア実装によって、復号化されたデータに有効なパディングがあるかどうかを確認できます。 Oracle は、"Invalid padding" という値を返すのと同じくらい単純なものか、無効なブロックではなく有効なブロックの処理に時間がかかるような複雑なものになる可能性があります。

ブロックベースの暗号には、モードと呼ばれる別のプロパティがあります。モードは、最初のブロック内のデータと 2 番目のブロック内のデータの関係を決定します。 最も一般的に使用されるモードの 1 つは CBC です。 CBC は、初期化ベクトル (IV) と呼ばれる初期ランダム ブロックを導入し、前のブロックを静的暗号化の結果と組み合わせて、同じキーで同じメッセージを暗号化しても必ずしも同じ暗号化された出力が生成されないようにします。

攻撃者は、埋め込みオラクルを CBC データの構造と組み合わせて使用して、Oracle を公開するコードに少し変更されたメッセージを送信し、Oracle からデータが正しいと伝えられるまでデータを送信し続けることができます。 この応答から、攻撃者はメッセージをバイト単位で復号化できます。

最新のコンピューター ネットワークは、攻撃者がリモート システムでの実行時間の非常に小さい (0.1 ミリ秒未満) の違いを検出できるほど高品質です。 暗号化解除が成功したと想定されているアプリケーションは、データが改ざんされていない場合にのみ発生する可能性があります。これは、復号化の成功と失敗の違いを観察するように設計されたツールからの攻撃に対して脆弱である可能性があります。 このタイミングの違いは、一部の言語やライブラリでは他の言語やライブラリよりも重要な場合もありますが、障害に対するアプリケーションの応答を考慮すると、これはすべての言語とライブラリにとって実際的な脅威であると考えられています。

この攻撃は、暗号化されたデータを変更し、Oracle で結果をテストする機能に依存します。 攻撃を完全に軽減する唯一の方法は、暗号化されたデータへの変更を検出し、それに対するアクションの実行を拒否することです。 これを行う標準的な方法は、データの署名を作成し、操作を実行する前にその署名を検証することです。 署名は検証可能である必要があります。攻撃者が作成することはできません。そうしないと、暗号化されたデータが変更され、変更されたデータに基づいて新しい署名が計算されます。 適切な署名の一般的な種類の 1 つは、キー付きハッシュ メッセージ認証コード (HMAC) と呼ばれます。 HMAC は、HMAC を生成するユーザーと検証するユーザーのみが認識する秘密鍵を受け取るという点でチェックサムとは異なります。 キーを所有しないと、正しい HMAC を生成できません。 データを受信すると、暗号化されたデータを取得し、自分と送信者共有の秘密鍵を使用して HMAC を個別に計算し、送信した HMAC を計算した HMAC と比較します。 この比較は一定の時間である必要があります。それ以外の場合は、別の検出可能なオラクルを追加し、別の種類の攻撃を許可します。

要約すると、埋め込まれた CBC ブロック暗号を安全に使用するには、データの暗号化を解除する前に、一定の時間比較を使用して検証する HMAC (または別のデータ整合性チェック) と組み合わせる必要があります。 変更されたすべてのメッセージは応答の生成に同じ時間がかかるため、攻撃は防止されます。

脆弱なユーザー

この脆弱性は、独自の暗号化と暗号化解除を実行しているマネージド アプリケーションとネイティブ アプリケーションの両方に適用されます。 これには、次の例が含まれます。

  • 後でサーバー上で復号化するために Cookie を暗号化するアプリケーション。
  • 後で暗号化が解除された列を持つテーブルにデータを挿入する機能をユーザーに提供するデータベース アプリケーション。
  • 転送中のデータを保護するために共有キーを使用した暗号化に依存するデータ転送アプリケーション。
  • TLS トンネル内でメッセージを暗号化および復号化するアプリケーション。

これらのシナリオでは、TLS のみを使用しても保護されない場合があることに注意してください。

脆弱なアプリケーション:

  • PKCS#7 や ANSI X.923 などの検証可能なパディング モードで CBC 暗号モードを使用してデータを復号化します。
  • (MAC または非対称デジタル署名を使用して) データ整合性チェックを実行せずに復号化を実行します。

これは、暗号化メッセージ構文 (PKCS#7/CMS) EnvelopedData 構造体など、これらのプリミティブ上の抽象化に基づいて構築されたアプリケーションにも適用されます。

調査により、Microsoft は、既知のフッター構造または予測可能なフッター構造がある場合に、ISO 10126 と同等の埋め込みが埋め込まれた CBC メッセージについてさらに懸念しています。 たとえば、W3C XML 暗号化の構文と処理に関する推奨事項 (xmlenc、EncryptedXml) の規則に従って準備されたコンテンツなどです。 メッセージに署名する W3C ガイダンスでは、その時点で暗号化が適切と見なされていましたが、Microsoft では常に暗号化してから署名を行うことをお勧めします。

アプリケーション開発者は、非対称キーと任意のメッセージの間に固有の信頼関係がないため、非対称署名キーの適用性を確認することに常に留意する必要があります。

詳細

これまで、HMAC や RSA 署名などの手段を使用して、重要なデータの暗号化と認証の両方が重要であるというコンセンサスが存在していました。 ただし、暗号化と認証の操作をシーケンスする方法に関するガイダンスはあまり明確ではありません。 この記事で詳しく説明されている脆弱性により、Microsoft のガイダンスでは常に "encrypt-then-sign" パラダイムを使用するようになりました。 つまり、まず対称キーを使用してデータを暗号化し、次に暗号テキスト (暗号化されたデータ) を介して MAC または非対称署名を計算します。 データの暗号化を解除する場合は、逆の操作を実行します。 まず、暗号テキストの MAC または署名を確認してから、暗号化を解除します。

"埋め込みオラクル攻撃" と呼ばれる一種の脆弱性は、10 年以上にわたって存在することが知られています。 これらの脆弱性により、攻撃者は、AES や 3DES などの対称ブロック アルゴリズムによって暗号化されたデータを暗号化解除し、データ ブロックごとに 4096 回以下の試行を行う可能性があります。 これらの脆弱性は、ブロック暗号が最後に検証可能なパディング データで最も頻繁に使用されるという事実を利用します。 攻撃者が暗号文を改ざんし、改ざんが最後にパディングの形式でエラーを引き起こしたかどうかを調べる場合、攻撃者はデータを復号化できることがわかりました。

当初、実用的な攻撃は、埋め込みが有効かどうかに基づいて異なるエラー コードを返すサービス (ASP.NET 脆弱性 MS10-070 など) に基づいて行われました。 ただし、有効なパディングと無効なパディングの処理のタイミングの違いのみを使用して、同様の攻撃を実行することが実用的であると Microsoft は考えています。

暗号化スキームが署名を使用し、署名検証が特定の長さのデータに対して固定ランタイムで実行される場合 (内容に関係なく)、 サイド チャネルを介して攻撃者に情報を出力することなく、データの整合性を検証できます。 整合性チェックでは改ざんされたメッセージが拒否されるため、パディング Oracle の脅威は軽減されます。

指導

まず第一に、機密性を持つデータは、Secure Sockets Layer (SSL) の後継であるトランスポート層セキュリティ (TLS) 経由で送信することをお勧めします。

次に、アプリケーションを分析して次の手順を実行します。

  • 実行している暗号化と、使用しているプラットフォームと API によって提供されている暗号化を正確に理解します。
  • AES や 3DES などの対称 ブロック暗号アルゴリズムの各レイヤーで、CBC モードでの各使用に、秘密鍵付きデータ整合性チェック (非対称署名、HMAC、または GCM や CCM などの 認証された暗号化 (AE) モードへの変更) が組み込まれている必要があります。

現在の調査に基づいて、暗号化の非 AE モードに対して認証と暗号化の手順を個別に実行する場合、暗号化テキスト (暗号化してから署名) を認証することが最善の一般的なオプションであると一般的に考えられています。 ただし、暗号化に対する万能の正解はなく、この一般化はプロの暗号業者からの指示されたアドバイスほど良くありません。

メッセージング形式を変更できないが、認証されていない CBC 復号化を実行するアプリケーションは、次のような軽減策を組み込むことをお勧めします。

  • 暗号化解除を許可せずに、パディングを検証または削除せずに復号化します。
    • 適用されたパディングは、引き続き削除または無視する必要があります。アプリケーションに負担を移します。
    • 利点は、パディングの検証と削除を他のアプリケーション データ検証ロジックに組み込むことができるということです。 埋め込み検証とデータ検証を一定の時間で実行できる場合、脅威は軽減されます。
    • パディングの解釈によって、認識されるメッセージの長さが変わるため、このアプローチから出力されるタイミング情報が存在する可能性があります。
  • 復号化パディング モードをISO10126に変更します。
    • ISO10126復号化パディングは、PKCS7 暗号化パディングとANSIX923暗号化パディングの両方と互換性があります。
    • モードを変更すると、埋め込み Oracle の知識がブロック全体ではなく 1 バイトに減ります。 ただし、コンテンツに既知のフッター (終了 XML 要素など) がある場合は、関連する攻撃がメッセージの残りの部分を攻撃し続ける可能性があります。
    • これにより、攻撃者が同じプレーンテキストを別のメッセージ オフセットで複数回暗号化するように強制できる状況でも、プレーンテキストの回復を防ぐことはありません。
  • 復号化呼び出しの評価をゲートして、タイミング信号を減衰させます。
    • 保持時間の計算には、パディングを含むデータ セグメントに対する復号化操作にかかる最大時間を超える最小値が必要です。
    • 時間の計算は、Environment.TickCount (ロールオーバー/オーバーフローの影響を受ける) または 2 つのシステム タイムスタンプ (NTP 調整エラーの影響を受ける) の減算ではなく、高解像度タイム スタンプの取得に関するガイダンスに従って行う必要があります。
    • 時間計算は、末尾に埋め込まれるだけでなく、マネージド アプリケーションまたは C++ アプリケーションのすべての潜在的な例外を含む復号化操作を含む必要があります。
    • 成功または失敗がまだ決定されている場合は、タイミング ゲートが期限切れになったときにエラーを返す必要があります。
  • 認証されていない暗号化解除を実行しているサービスでは、"無効な" メッセージが大量に送信されたことを検出するための監視を実施する必要があります。
    • このシグナルには、誤検知 (正当に破損したデータ) と偽陰性 (検出を回避するために十分に長い時間にわたって攻撃が拡散される) の両方が含まれる点に注意してください。

脆弱なコードの検索 - ネイティブ アプリケーション

Windows Cryptography: Next Generation (CNG) ライブラリに対してビルドされたプログラムの場合:

  • 復号化呼び出しは、BCRYPT_BLOCK_PADDING フラグを指定して BCryptDecrypt に対して行います。
  • キー ハンドルは、BCRYPT_CHAINING_MODEを BCRYPT_CHAIN_MODE_CBC に設定して BCryptSetProperty を呼び出すことによって初期化されました。
    • BCRYPT_CHAIN_MODE_CBCが既定値であるため、影響を受けるコードでは、BCRYPT_CHAINING_MODEの値が割り当てられない可能性があります。

以前の Windows 暗号化 API に対してビルドされたプログラムの場合:

  • 復号化呼び出しは、Final=TRUEを使用して CryptDecrypt に対して行います。
  • キー ハンドルは、KP_MODEを CRYPT_MODE_CBCに設定してCryptSetKeyParam を呼び出すことによって初期化されました。
    • CRYPT_MODE_CBCが既定値であるため、影響を受けるコードでは、KP_MODEの値が割り当てられない可能性があります。

脆弱なコードの検索 - マネージド アプリケーション

脆弱なコードの検索 - 暗号化メッセージの構文

暗号化されたコンテンツが AES (2.16.840.1.101.3.4.1.2、2.16.840.1.101.3.4.1.22) の CBC モードを使用する認証されていない CMS EnvelopedData メッセージ 2.16.840.1.101.3.4.1.42、DES (1.3.14.3.2.7)、3DES (1.2.840.113549.3.7) または RC2 (1.2.840.113549.3.2) は脆弱です。 また、CBC モードで他のブロック暗号アルゴリズムを使用するメッセージも含まれます。

ストリーム暗号は、この特定の脆弱性の影響を受けにくいですが、Microsoft では、ContentEncryptionAlgorithm 値を検査するよりも常にデータを認証することをお勧めします。

マネージド アプリケーションの場合、CMS EnvelopedData BLOB は、 System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[])に渡される任意の値として検出できます。

ネイティブ アプリケーションの場合、CMS EnvelopedData BLOB は、CryptMsgUpdate を介して CMS ハンドルに提供される任意の値として検出できます。その結果、CMSG_TYPE_PARAMCMSG_ENVELOPEDされるか、CMS ハンドルが後で CryptMsgControl 経由でCMSG_CTRL_DECRYPT命令を送信されます。

脆弱なコード例 - マネージド

このメソッドは Cookie を読み取って暗号化を解除し、データ整合性チェックは表示されません。 そのため、この方法で読み取られる Cookie の内容は、それを受け取ったユーザーや、暗号化された Cookie 値を取得した攻撃者によって攻撃される可能性があります。

private byte[] DecryptCookie(string cookieName)
{
    HttpCookie cookie = Request.Cookies[cookieName];

    if (cookie == null)
    {
        return null;
    }

    using (ICryptoTransform decryptor = _aes.CreateDecryptor())
    using (MemoryStream memoryStream = new MemoryStream())
    using (CryptoStream cryptoStream =
        new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
    {
        byte[] readCookie = Convert.FromBase64String(cookie.Value);
        cryptoStream.Write(readCookie, 0, readCookie.Length);
        cryptoStream.FlushFinalBlock();
        return memoryStream.ToArray();
    }
}

次のサンプル コードでは、標準以外のメッセージ形式を使用します。

cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext

ここで、 cipher_algorithm_id および hmac_algorithm_id アルゴリズム識別子は、これらのアルゴリズムのアプリケーション ローカル (非標準) 表現です。 これらの識別子は、ベア連結バイトストリームとしてではなく、既存のメッセージング プロトコルの他の部分で意味を持つ場合があります。

この例では、1 つのマスター キーを使用して、暗号化キーと HMAC キーの両方を派生させます。 これは、1 つのキーを持つアプリケーションをデュアル キーアプリケーションに変換する利便性と、2 つのキーを異なる値として保持することを奨励する利便性の両方として提供されます。 さらに、HMAC キーと暗号化キーが同期から抜け出すことはできません。

このサンプルでは、暗号化または暗号化解除の Stream は受け入れられません。 現在のデータ形式では、 hmac_tag 値が暗号テキストの前にあるため、ワンパス暗号化が困難になります。 ただし、この形式は、パーサーをシンプルに保つために、すべての固定サイズ要素を最初に保持するため選択されました。 このデータ形式では、ワンパス復号化が可能ですが、実装者は、TransformFinalBlock を呼び出す前に GetHashAndReset を呼び出し、結果を確認するように注意してください。 ストリーミング暗号化が重要な場合は、別の AE モードが必要になることがあります。

// ==++==
//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
//   Shared under the terms of the Microsoft Public License,
//   https://opensource.org/licenses/MS-PL
//
// ==--==

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;

namespace Microsoft.Examples.Cryptography
{
    public enum AeCipher : byte
    {
        Unknown,
        Aes256CbcPkcs7,
    }

    public enum AeMac : byte
    {
        Unknown,
        HMACSHA256,
        HMACSHA384,
    }

    /// <summary>
    /// Provides extension methods to make HashAlgorithm look like .NET Core's
    /// IncrementalHash
    /// </summary>
    internal static class IncrementalHashExtensions
    {
        public static void AppendData(this HashAlgorithm hash, byte[] data)
        {
            hash.TransformBlock(data, 0, data.Length, null, 0);
        }

        public static void AppendData(
            this HashAlgorithm hash,
            byte[] data,
            int offset,
            int length)
        {
            hash.TransformBlock(data, offset, length, null, 0);
        }

        public static byte[] GetHashAndReset(this HashAlgorithm hash)
        {
            hash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
            return hash.Hash;
        }
    }

    public static partial class AuthenticatedEncryption
    {
        /// <summary>
        /// Use <paramref name="masterKey"/> to derive two keys (one cipher, one HMAC)
        /// to provide authenticated encryption for <paramref name="message"/>.
        /// </summary>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <param name="message">The message to encrypt</param>
        /// <returns>
        /// A concatenation of
        /// [cipher algorithm+chainmode+padding][mac algorithm][authtag][IV][ciphertext],
        /// suitable to be passed to <see cref="Decrypt"/>.
        /// </returns>
        /// <remarks>
        /// <paramref name="masterKey"/> should be a 128-bit (or bigger) value generated
        /// by a secure random number generator, such as the one returned from
        /// <see cref="RandomNumberGenerator.Create()"/>.
        /// This implementation chooses to block deficient inputs by length, but does not
        /// make any attempt at discerning the randomness of the key.
        ///
        /// If the master key is being input by a prompt (like a password/passphrase)
        /// then it should be properly turned into keying material via a Key Derivation
        /// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
        /// never be simply turned to bytes via an Encoding class and used as a key.
        /// </remarks>
        public static byte[] Encrypt(byte[] masterKey, byte[] message)
        {
            if (masterKey == null)
                throw new ArgumentNullException(nameof(masterKey));
            if (masterKey.Length < 16)
                throw new ArgumentOutOfRangeException(
                    nameof(masterKey),
                    "Master Key must be at least 128 bits (16 bytes)");
            if (message == null)
                throw new ArgumentNullException(nameof(message));

            // First, choose an encryption scheme.
            AeCipher aeCipher = AeCipher.Aes256CbcPkcs7;

            // Second, choose an authentication (message integrity) scheme.
            //
            // In this example we use the master key length to change from HMACSHA256 to
            // HMACSHA384, but that is completely arbitrary. This mostly represents a
            // "cryptographic needs change over time" scenario.
            AeMac aeMac = masterKey.Length < 48 ? AeMac.HMACSHA256 : AeMac.HMACSHA384;

            // It's good to be able to identify what choices were made when a message was
            // encrypted, so that the message can later be decrypted. This allows for
            // future versions to add support for new encryption schemes, but still be
            // able to read old data. A practice known as "cryptographic agility".
            //
            // This is similar in practice to PKCS#7 messaging, but this uses a
            // private-scoped byte rather than a public-scoped Object IDentifier (OID).
            // Please note that the scheme in this example adheres to no particular
            // standard, and is unlikely to survive to a more complete implementation in
            // the .NET Framework.
            //
            // You may be well-served by prepending a version number byte to this
            // message, but may want to avoid the value 0x30 (the leading byte value for
            // DER-encoded structures such as X.509 certificates and PKCS#7 messages).
            byte[] algorithmChoices = { (byte)aeCipher, (byte)aeMac };
            byte[] iv;
            byte[] cipherText;
            byte[] tag;

            // Using our algorithm choices, open an HMAC (as an authentication tag
            // generator) and a SymmetricAlgorithm which use different keys each derived
            // from the same master key.
            //
            // A custom implementation may very well have distinctly managed secret keys
            // for the MAC and cipher, this example merely demonstrates the master to
            // derived key methodology to encourage key separation from the MAC and
            // cipher keys.
            using (HMAC tagGenerator = GetMac(aeMac, masterKey))
            {
                using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
                using (ICryptoTransform encryptor = cipher.CreateEncryptor())
                {
                    // Since no IV was provided, a random one has been generated
                    // during the call to CreateEncryptor.
                    //
                    // But note that it only does the auto-generation once. If the cipher
                    // object were used again, a call to GenerateIV would have been
                    // required.
                    iv = cipher.IV;

                    cipherText = Transform(encryptor, message, 0, message.Length);
                }

                // The IV and ciphertext both need to be included in the MAC to prevent
                // tampering.
                //
                // By including the algorithm identifiers, we have technically moved from
                // simple Authenticated Encryption (AE) to Authenticated Encryption with
                // Additional Data (AEAD). By including the algorithm identifiers in the
                // MAC, it becomes harder for an attacker to change them as an attempt to
                // perform a downgrade attack.
                //
                // If you've added a data format version field, it can also be included
                // in the MAC to further inhibit an attacker's options for confusing the
                // data processor into believing the tampered message is valid.
                tagGenerator.AppendData(algorithmChoices);
                tagGenerator.AppendData(iv);
                tagGenerator.AppendData(cipherText);
                tag = tagGenerator.GetHashAndReset();
            }

            // Build the final result as the concatenation of everything except the keys.
            int totalLength =
                algorithmChoices.Length +
                tag.Length +
                iv.Length +
                cipherText.Length;

            byte[] output = new byte[totalLength];
            int outputOffset = 0;

            Append(algorithmChoices, output, ref outputOffset);
            Append(tag, output, ref outputOffset);
            Append(iv, output, ref outputOffset);
            Append(cipherText, output, ref outputOffset);

            Debug.Assert(outputOffset == output.Length);
            return output;
        }

        /// <summary>
        /// Reads a message produced by <see cref="Encrypt"/> after verifying it hasn't
        /// been tampered with.
        /// </summary>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <param name="cipherText">
        /// The output of <see cref="Encrypt"/>: a concatenation of a cipher ID, mac ID,
        /// authTag, IV, and cipherText.
        /// </param>
        /// <returns>The decrypted content.</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="masterKey"/> is <c>null</c>.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="cipherText"/> is <c>null</c>.
        /// </exception>
        /// <exception cref="CryptographicException">
        /// <paramref name="cipherText"/> identifies unknown algorithms, is not long
        /// enough, fails a data integrity check, or fails to decrypt.
        /// </exception>
        /// <remarks>
        /// <paramref name="masterKey"/> should be a 128-bit (or larger) value
        /// generated by a secure random number generator, such as the one returned from
        /// <see cref="RandomNumberGenerator.Create()"/>. This implementation chooses to
        /// block deficient inputs by length, but doesn't make any attempt at
        /// discerning the randomness of the key.
        ///
        /// If the master key is being input by a prompt (like a password/passphrase),
        /// then it should be properly turned into keying material via a Key Derivation
        /// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
        /// never be simply turned to bytes via an Encoding class and used as a key.
        /// </remarks>
        public static byte[] Decrypt(byte[] masterKey, byte[] cipherText)
        {
            // This example continues the .NET practice of throwing exceptions for
            // failures. If you consider message tampering to be normal (and thus
            // "not exceptional") behavior, you may like the signature
            // bool Decrypt(byte[] messageKey, byte[] cipherText, out byte[] message)
            // better.
            if (masterKey == null)
                throw new ArgumentNullException(nameof(masterKey));
            if (masterKey.Length < 16)
                throw new ArgumentOutOfRangeException(
                    nameof(masterKey),
                    "Master Key must be at least 128 bits (16 bytes)");
            if (cipherText == null)
                throw new ArgumentNullException(nameof(cipherText));

            // The format of this message is assumed to be public, so there's no harm in
            // saying ahead of time that the message makes no sense.
            if (cipherText.Length < 2)
            {
                throw new CryptographicException();
            }

            // Use the message algorithm headers to determine what cipher algorithm and
            // MAC algorithm are going to be used. Since the same Key Derivation
            // Functions (KDFs) are being used in Decrypt as Encrypt, the keys are also
            // the same.
            AeCipher aeCipher = (AeCipher)cipherText[0];
            AeMac aeMac = (AeMac)cipherText[1];

            using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
            using (HMAC tagGenerator = GetMac(aeMac, masterKey))
            {
                int blockSizeInBytes = cipher.BlockSize / 8;
                int tagSizeInBytes = tagGenerator.HashSize / 8;
                int headerSizeInBytes = 2;
                int tagOffset = headerSizeInBytes;
                int ivOffset = tagOffset + tagSizeInBytes;
                int cipherTextOffset = ivOffset + blockSizeInBytes;
                int cipherTextLength = cipherText.Length - cipherTextOffset;
                int minLen = cipherTextOffset + blockSizeInBytes;

                // Again, the minimum length is still assumed to be public knowledge,
                // nothing has leaked out yet. The minimum length couldn't just be calculated
                // without reading the header.
                if (cipherText.Length < minLen)
                {
                    throw new CryptographicException();
                }

                // It's very important that the MAC be calculated and verified before
                // proceeding to decrypt the ciphertext, as this prevents any sort of
                // information leaking out to an attacker.
                //
                // Don't include the tag in the calculation, though.

                // First, everything before the tag (the cipher and MAC algorithm ids)
                tagGenerator.AppendData(cipherText, 0, tagOffset);

                // Skip the data before the tag and the tag, then read everything that
                // remains.
                tagGenerator.AppendData(
                    cipherText,
                    tagOffset + tagSizeInBytes,
                    cipherText.Length - tagSizeInBytes - tagOffset);

                byte[] generatedTag = tagGenerator.GetHashAndReset();

                // The time it took to get to this point has so far been a function only
                // of the length of the data, or of non-encrypted values (for example, it could
                // take longer to prepare the *key* for the HMACSHA384 MAC than the
                // HMACSHA256 MAC, but the algorithm choice wasn't a secret).
                //
                // If the verification of the authentication tag aborts as soon as a
                // difference is found in the byte arrays then your program may be
                // acting as a timing oracle which helps an attacker to brute-force the
                // right answer for the MAC. So, it's very important that every possible
                // "no" (2^256-1 of them for HMACSHA256) be evaluated in as close to the
                // same amount of time as possible. For this, we call CryptographicEquals
                if (!CryptographicEquals(
                    generatedTag,
                    0,
                    cipherText,
                    tagOffset,
                    tagSizeInBytes))
                {
                    // Assuming every tampered message (of the same length) took the same
                    // amount of time to process, we can now safely say
                    // "this data makes no sense" without giving anything away.
                    throw new CryptographicException();
                }

                // Restore the IV into the symmetricAlgorithm instance.
                byte[] iv = new byte[blockSizeInBytes];
                Buffer.BlockCopy(cipherText, ivOffset, iv, 0, iv.Length);
                cipher.IV = iv;

                using (ICryptoTransform decryptor = cipher.CreateDecryptor())
                {
                    return Transform(
                        decryptor,
                        cipherText,
                        cipherTextOffset,
                        cipherTextLength);
                }
            }
        }

        private static byte[] Transform(
            ICryptoTransform transform,
            byte[] input,
            int inputOffset,
            int inputLength)
        {
            // Many of the implementations of ICryptoTransform report true for
            // CanTransformMultipleBlocks, and when the entire message is available in
            // one shot this saves on the allocation of the CryptoStream and the
            // intermediate structures it needs to properly chunk the message into blocks
            // (since the underlying stream won't always return the number of bytes
            // needed).
            if (transform.CanTransformMultipleBlocks)
            {
                return transform.TransformFinalBlock(input, inputOffset, inputLength);
            }

            // If our transform couldn't do multiple blocks at once, let CryptoStream
            // handle the chunking.
            using (MemoryStream messageStream = new MemoryStream())
            using (CryptoStream cryptoStream =
                new CryptoStream(messageStream, transform, CryptoStreamMode.Write))
            {
                cryptoStream.Write(input, inputOffset, inputLength);
                cryptoStream.FlushFinalBlock();
                return messageStream.ToArray();
            }
        }

        /// <summary>
        /// Open a properly configured <see cref="SymmetricAlgorithm"/> conforming to the
        /// scheme identified by <paramref name="aeCipher"/>.
        /// </summary>
        /// <param name="aeCipher">The cipher mode to open.</param>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <returns>
        /// A SymmetricAlgorithm object with the right key, cipher mode, and padding
        /// mode; or <c>null</c> on unknown algorithms.
        /// </returns>
        private static SymmetricAlgorithm GetCipher(AeCipher aeCipher, byte[] masterKey)
        {
            SymmetricAlgorithm symmetricAlgorithm;

            switch (aeCipher)
            {
                case AeCipher.Aes256CbcPkcs7:
                    symmetricAlgorithm = Aes.Create();
                    // While 256-bit, CBC, and PKCS7 are all the default values for these
                    // properties, being explicit helps comprehension more than it hurts
                    // performance.
                    symmetricAlgorithm.KeySize = 256;
                    symmetricAlgorithm.Mode = CipherMode.CBC;
                    symmetricAlgorithm.Padding = PaddingMode.PKCS7;
                    break;
                default:
                    // An algorithm we don't understand
                    throw new CryptographicException();
            }

            // Instead of using the master key directly, derive a key for our chosen
            // HMAC algorithm based upon the master key.
            //
            // Since none of the symmetric encryption algorithms currently in .NET
            // support key sizes greater than 256-bit, we can use HMACSHA256 with
            // NIST SP 800-108 5.1 (Counter Mode KDF) to derive a value that is
            // no smaller than the key size, then Array.Resize to trim it down as
            // needed.

            using (HMAC hmac = new HMACSHA256(masterKey))
            {
                // i=1, Label=ASCII(cipher)
                byte[] cipherKey = hmac.ComputeHash(
                    new byte[] { 1, 99, 105, 112, 104, 101, 114 });

                // Resize the array to the desired keysize. KeySize is in bits,
                // and Array.Resize wants the length in bytes.
                Array.Resize(ref cipherKey, symmetricAlgorithm.KeySize / 8);

                symmetricAlgorithm.Key = cipherKey;
            }

            return symmetricAlgorithm;
        }

        /// <summary>
        /// Open a properly configured <see cref="HMAC"/> conforming to the scheme
        /// identified by <paramref name="aeMac"/>.
        /// </summary>
        /// <param name="aeMac">The message authentication mode to open.</param>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <returns>
        /// An HMAC object with the proper key, or <c>null</c> on unknown algorithms.
        /// </returns>
        private static HMAC GetMac(AeMac aeMac, byte[] masterKey)
        {
            HMAC hmac;

            switch (aeMac)
            {
                case AeMac.HMACSHA256:
                    hmac = new HMACSHA256();
                    break;
                case AeMac.HMACSHA384:
                    hmac = new HMACSHA384();
                    break;
                default:
                    // An algorithm we don't understand
                    throw new CryptographicException();
            }

            // Instead of using the master key directly, derive a key for our chosen
            // HMAC algorithm based upon the master key.
            // Since the output size of the HMAC is the same as the ideal key size for
            // the HMAC, we can use the master key over a fixed input once to perform
            // NIST SP 800-108 5.1 (Counter Mode KDF):
            hmac.Key = masterKey;

            // i=1, Context=ASCII(MAC)
            byte[] newKey = hmac.ComputeHash(new byte[] { 1, 77, 65, 67 });

            hmac.Key = newKey;
            return hmac;
        }

        // A simple helper method to ensure that the offset (writePos) always moves
        // forward with new data.
        private static void Append(byte[] newData, byte[] combinedData, ref int writePos)
        {
            Buffer.BlockCopy(newData, 0, combinedData, writePos, newData.Length);
            writePos += newData.Length;
        }

        /// <summary>
        /// Compare the contents of two arrays in an amount of time which is only
        /// dependent on <paramref name="length"/>.
        /// </summary>
        /// <param name="a">An array to compare to <paramref name="b"/>.</param>
        /// <param name="aOffset">
        /// The starting position within <paramref name="a"/> for comparison.
        /// </param>
        /// <param name="b">An array to compare to <paramref name="a"/>.</param>
        /// <param name="bOffset">
        /// The starting position within <paramref name="b"/> for comparison.
        /// </param>
        /// <param name="length">
        /// The number of bytes to compare between <paramref name="a"/> and
        /// <paramref name="b"/>.</param>
        /// <returns>
        /// <c>true</c> if both <paramref name="a"/> and <paramref name="b"/> have
        /// sufficient length for the comparison and all of the applicable values are the
        /// same in both arrays; <c>false</c> otherwise.
        /// </returns>
        /// <remarks>
        /// An "insufficient data" <c>false</c> response can happen early, but otherwise
        /// a <c>true</c> or <c>false</c> response take the same amount of time.
        /// </remarks>
        [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
        private static bool CryptographicEquals(
            byte[] a,
            int aOffset,
            byte[] b,
            int bOffset,
            int length)
        {
            Debug.Assert(a != null);
            Debug.Assert(b != null);
            Debug.Assert(length >= 0);

            int result = 0;

            if (a.Length - aOffset < length || b.Length - bOffset < length)
            {
                return false;
            }

            unchecked
            {
                for (int i = 0; i < length; i++)
                {
                    // Bitwise-OR of subtraction has been found to have the most
                    // stable execution time.
                    //
                    // This cannot overflow because bytes are 1 byte in length, and
                    // result is 4 bytes.
                    // The OR propagates all set bytes, so the differences are only
                    // present in the lowest byte.
                    result = result | (a[i + aOffset] - b[i + bOffset]);
                }
            }

            return result == 0;
        }
    }
}

こちらも参照ください