パディングを使用した CBC モードの対称復号化に関するタイミングの脆弱性

Microsoft は、極めて特定の状況を除き、暗号化テキストの整合性を最初に確認することなく、検証可能なパディングを適用すると、対称暗号化の暗号ブロック チェーン (CBC) モードで暗号化されたデータを復号化することは、もはや安全ではないと考えます。 この判断は、現在知られている暗号化の調査に基づいています。

はじめに

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

オラクルとは、実行しているアクションが正しいかどうかについての情報を攻撃者に与える "お告げ" のことです。 子供とボード ゲームやカード ゲームをしているところを想像してください。 その顔が輝いて満面の笑みが浮かんだら、自分が勝ちそうだと考えているためです。それがオラクルです。 対戦相手であるあなたは、このオラクルを利用して、次の手を適切に計画することができます。

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

この 2 つを組み合わせることで、パディング オラクルを使用するソフトウェアの実装により、復号化されたデータのパディングが有効なものかどうかが明らかになります。 オラクルは、"無効なパディング" であることを示す値を返すだけの単純なものから、有効なブロックと無効なブロックの処理時間の明白な違いを取得するような複雑なものまであります。

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

攻撃者は、CBC データの構成方法と組み合わせて、パディング オラクルを使用して、オラクルを公開するコードにわずかに変更されたメッセージを送信し、データが正しいことをオラクルが告げるまでデータを送信し続けることができます。 この応答から、攻撃者はメッセージをバイト単位で復号化できます。

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

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

まとめると、パディングされた CBC ブロック暗号を安全に使用するには、データを復号化する前に一定時間の比較を使用して検証を行う HMAC (または他のデータ整合性チェック) と組み合わせて、使用する必要があります。 変更されたすべてのメッセージについて、応答の生成にかかる時間が同じなので、攻撃を防ぐことができます。

脆弱なユーザー

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

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

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

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

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

これは、Cryptographic Message Syntax (PKCS#7/CMS) EnvelopedData 構造体などのプリミティブ上の抽象化を基に構築されたアプリケーションにも当てはまります。

研究により、Microsoft は、メッセージで既知または予測可能なフッター構造が使用されている場合に、ISO 10126 と同等のパディングを使用している CBC メッセージについて、さらに懸念しています。 たとえば、W3C の XML 暗号化構文と処理の推奨事項 (xmlenc、EncryptedXml) の規則に基づいて準備されているコンテンツなどです。 メッセージに署名してから暗号化するという W3C のガイダンスは当時は適切であると考えられていましたが、現在、Microsoft は常に暗号化してから署名することをお勧めします。

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

詳細

これまで、HMAC や RSA 署名などの方法を使用して、重要なデータを暗号化して認証することが重要であるという合意がありました。 しかし、暗号化操作と認証操作をどのような順序で行うかということについては、明確なガイダンスはありません。 この記事で説明されている脆弱性により、Microsoft のガイダンスは、常に "暗号化してから署名" というパラダイムを使用することになっています。 つまり、最初に対称キーを使用してデータを暗号化した後、暗号化テキスト (暗号化されたデータ) に対して MAC または非対称署名を計算します。 データを復号化するときは、逆の操作を実行します。 最初に、暗号化テキストの MAC または署名を確認してから、それを復号化します。

"パディング オラクル攻撃" と呼ばれる脆弱性のクラスは、10 年以上前から存在することがわかっています。 これらの脆弱性により、攻撃者は、AES や3DES などの対称ブロック アルゴリズムによって暗号化されたデータを、データのブロックあたり 4096 回以下の試行で、復号化することができます。 これらの脆弱性では、ブロック暗号には末尾の検証可能なパディング データが最も頻繁に使用されるという事実が利用されています。 攻撃者が暗号化テキストを改ざんし、改ざんによって末尾のパディングの形式でエラーが発生するかどうかを確認できる場合、攻撃者はデータを復号化できるということがわかりました。

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

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

ガイダンス

何よりもまず、機密性を必要とするすべてのデータは、Secure Sockets Layer (SSL) の後継であるトランスポート層セキュリティ (TLS) を使用して送信することを、Microsoft はお勧めします。

次に、以下のためにアプリケーションを分析します。

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

最新の研究に基づき、認証と暗号化の手順が非 AE モードの暗号化に対して個別に実行されるときは、暗号化テキストを認証すること (暗号化してから署名) が最適なオプションであると一般に考えられます。 ただし、暗号化にはどんな場合にも通用する正しい答えはなく、この一般化は、暗号の専門家からの具体的なアドバイスほど良いものではありません。

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

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

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

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

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

以前の Windows Cryptographic API を対象にビルドされたプログラムの場合:

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

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

脆弱性のあるコードの検出 - 暗号化メッセージの構文

暗号化されたコンテンツで使用されている CBC モードが AES (2.16.840.1.101.3.4.1.2、2.16.840.1.101.3.4.1.22、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) である認証されていない CMS EnvelopedData メッセージと、CBC モードで他のブロック暗号アルゴリズムが使用されているメッセージは脆弱です。

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

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

ネイティブ アプリケーションの場合、CMS EnvelopedData BLOB は、結果の CMSG_TYPE_PARAMCMSG_ENVELOPED である CryptMsgUpdate を介して CMS ハンドルに提供される任意の値として検出できます。または、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_idhmac_algorithm_id のアルゴリズム識別子は、それらのアルゴリズムのアプリケーション ローカルの (標準ではない) 表現です。 これらの識別子は、生の連結されたバイトストリームとしてではなく、既存のメッセージング プロトコルの他の部分で意味がある可能性があります。

また、この例では、1 つのマスター キーを使用して、暗号化キーと HMAC キーの両方が派生されています。 これは、シングルキー アプリケーションをデュアルキー アプリケーションに変換のに便利なことと、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 (e.g. 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;
        }
    }
}

関連項目