Luki w zabezpieczeniach chronometrażu z odszyfrowywaniem symetrycznym w trybie CBC przy użyciu uzupełnienia

Firma Microsoft uważa, że nie jest już bezpieczne odszyfrowywanie danych zaszyfrowanych przy użyciu trybu CBC (Cipher-Block-Chaining) szyfrowania symetrycznego, gdy zastosowano weryfikowalne dopełnienie bez uprzedniego zapewnienia integralności szyfru, z wyjątkiem bardzo konkretnych okoliczności. Jest to oparte na obecnie znanych badaniach kryptograficznych.

Wprowadzenie

Atak typu "wyrocznia uzupełnienia" jest typem ataku na zaszyfrowane dane, który umożliwia osobie atakującej odszyfrowanie zawartości danych bez znajomości klucza.

Wyrocznia odnosi się do "informacji", która dostarcza osobie atakującej informacje o tym, czy wykonywana akcja jest poprawna, czy nie. Imagine gra w planszę lub grę w kartę z dzieciem. Gdy ich twarz oświetla się dużym uśmiechem, ponieważ myślą, że chce wykonać dobry krok, jest to wyrocznia. Ty, jako wyrocznia, możesz użyć tej wyroczni do odpowiedniego zaplanowania następnego przeniesienia.

Wypełnienie to określony termin kryptograficzny. Niektóre szyfry, czyli algorytmy używane do szyfrowania danych, działają na blokach danych, w których każdy blok ma stały rozmiar. Jeśli dane, które chcesz zaszyfrować, nie są odpowiednim rozmiarem do wypełnienia bloków, dane są dopełniane, dopóki nie zostanie to zrobić. Wiele form uzupełnienia wymaga, aby wypełnienie zawsze było obecne, nawet jeśli oryginalne dane wejściowe były odpowiedniego rozmiaru. Dzięki temu wypełnienie może być zawsze bezpiecznie usuwane po odszyfrowywać.

Po umieszczeniu tych dwóch rzeczy implementacja oprogramowania z wyrocznią uzupełniania ujawnia, czy odszyfrowane dane mają prawidłowe dopełnienie. Wyrocznia może być tak prosta, jak zwrócenie wartości "Nieprawidłowe dopełnienie" lub bardziej skomplikowanej, na przykład czasu na przetworzyć prawidłowy blok, a nie nieprawidłowy blok.

Szyfry blokowe mają inną właściwość, nazywaną trybem, która określa relację danych w pierwszym bloku z danymi w drugim bloku i tak dalej. Jednym z najczęściej używanych trybów jest CBC. CBC wprowadza początkowy blok losowy, znany jako wektor inicjowania (IV), i łączy poprzedni blok z wynikiem szyfrowania statycznego w taki sposób, że zaszyfrowanie tego samego komunikatu za pomocą tego samego klucza nie zawsze spowoduje wygenerowanie tych samych zaszyfrowanych danych wyjściowych.

Osoba atakująca może użyć dopełnienia wyroczni w połączeniu ze strukturą danych CBC, aby wysyłać nieco zmienione komunikaty do kodu, który uwidacznia wyrocznię, i nadal wysyłać dane, dopóki wyrocznia nie poda informacji, że dane są poprawne. Z tej odpowiedzi osoba atakująca może odszyfrować bajt wiadomości.

Nowoczesne sieci komputerowe są tak wysokiej jakości, że osoba atakująca może wykryć bardzo małe (mniej niż 0,1 ms) różnice czasu wykonywania w systemach zdalnych. Aplikacje, które zakładają, że pomyślne odszyfrowywanie może nastąpić tylko wtedy, gdy dane nie zostały naruszone, mogą być narażone na ataki z narzędzi, które są przeznaczone do obserwowania różnic w pomyślnym i niepomyślnym odszyfrowywać. Chociaż ta różnica czasu może być bardziej znacząca w niektórych językach lub bibliotekach niż inne, teraz uważa się, że jest to praktyczne zagrożenie dla wszystkich języków i bibliotek, gdy uwzględniana jest odpowiedź aplikacji na awarię.

Ten atak opiera się na możliwości zmiany zaszyfrowanych danych i przetestowania wyniku wyroczni. Jedynym sposobem pełnego ograniczenia ataku jest wykrycie zmian zaszyfrowanych danych i odmowa wykonania na nim jakichkolwiek działań. Standardowym sposobem wykonania tej czynności jest utworzenie podpisu dla danych i zweryfikowanie tego podpisu przed rozpoczęciem jakichkolwiek operacji. Podpis musi być weryfikowalny, nie może zostać utworzony przez atakującego. W przeciwnym razie osoba atakująca zmieni zaszyfrowane dane, a następnie obliczy nowy podpis na podstawie zmienionych danych. Jeden wspólny typ odpowiedniego podpisu jest znany jako kod uwierzytelniania komunikatów skrótu klucza (HMAC). Klucz HMAC różni się od sumy kontrolnej tym, że przyjmuje klucz tajny, znany tylko osobie tworzącej klucz HMAC i osobie, która go wie. Bez posiadania klucza nie można utworzyć poprawnego klucza HMAC. Po otrzymaniu danych należy użyć zaszyfrowanych danych, niezależnie obliczyć klucz HMAC przy użyciu klucza tajnego oraz udziału nadawcy, a następnie porównać wysłany klucz HMAC z tym, który został obliczony. To porównanie musi być czasem stałym. W przeciwnym razie dodano kolejną wykrywaną wyrocznię, która zezwala na inny typ ataku.

Podsumowując, aby bezpiecznie używać szyfrów blokowych CBC, należy połączyć je z kluczem HMAC (lub innym sprawdzaniem integralności danych), który można zweryfikować przy użyciu stałego porównania czasu przed próbą odszyfrowania danych. Ponieważ reagowanie na wszystkie zmienione komunikaty trwa tyle samo czasu, atak jest blokowany.

KtoTo jest narażona na zagrożenia

Ta luka w zabezpieczeniach dotyczy zarówno aplikacji zarządzanych, jak i natywnych, które mają własne szyfrowanie i odszyfrowywanie. Obejmuje to na przykład:

  • Aplikacja, która szyfruje plik cookie do późniejszego odszyfrowania na serwerze.
  • Aplikacja bazy danych, która umożliwia użytkownikom wstawianie danych do tabeli, której kolumny są później odszyfrowywać.
  • Aplikacja do transferu danych, która opiera się na szyfrowaniu przy użyciu klucza wspólnego w celu ochrony przesyłanych danych.
  • Aplikacja, która szyfruje i odszyfrowuje komunikaty "wewnątrz" tunelu TLS.

Należy pamiętać, że korzystanie z samej usługi TLS może nie zapewnić ochrony w tych scenariuszach.

Podatna na zagrożenia aplikacja:

  • Odszyfrowuje dane przy użyciu trybu szyfrowania CBC z weryfikowalnym trybem dopełnienia, takim jak PKCS #7 lub ANSI X.923.
  • Wykonuje odszyfrowywanie bez sprawdzania integralności danych (za pośrednictwem komputera MAC lub asymetrycznego podpisu cyfrowego).

Dotyczy to również aplikacji zbudowanych na podstawie abstrakcji ponad tymi typami pierwotnymi, takich jak struktura Cryptographic Message Syntax (PKCS #7/CMS) EnvelopedData.

Badania doprowadziły firmę Microsoft do dalszego zaniepokojenie się komunikatami CBC, które są dopełniane uzupełnieniem równoważnym standardem ISO 10126, gdy komunikat ma dobrze znaną lub przewidywalną strukturę stopki. Na przykład zawartość przygotowana zgodnie z regułami zalecenia składni i przetwarzania szyfrowania XML W3C (xmlenc, EncryptedXml). Chociaż wskazówki dotyczące podpisywania komunikatu przez usługę W3C, a następnie szyfrowania, były wtedy uznawane za odpowiednie, firma Microsoft zaleca teraz, aby zawsze szyfrować, a następnie podpisywać.

Deweloperzy aplikacji powinni zawsze mieć na uwadze weryfikowanie możliwości zastosowania klucza podpisu asymetrycznego, ponieważ nie istnieje żadna relacja zaufania między kluczem asymetrycznym i dowolnym komunikatem.

Szczegóły

W przeszłości osiągnięto porozumienie, że ważne jest szyfrowanie i uwierzytelnianie ważnych danych przy użyciu środków takich jak podpisy HMAC lub RSA. Istnieją jednak mniej jasne wskazówki dotyczące sekwencji operacji szyfrowania i uwierzytelniania. Ze względu na lukę w zabezpieczeniach szczegółowo opisane w tym artykule wskazówki firmy Microsoft są teraz takie, aby zawsze używać paradygmatu "szyfruj, a następnie podpisz". Oznacza to, że najpierw szyfruj dane przy użyciu klucza symetrycznego, a następnie obliczaj podpis MAC lub asymetryczny na szyfrowanym (zaszyfrowanym danych). Podczas odszyfrowywania danych wykonaj odwrotną operacji. Najpierw potwierdź adres MAC lub podpis szyfru, a następnie odszyfruj go.

Od ponad 10 lat istnieje klasa luk w zabezpieczeniach znana jako "ataki na wyrocznię uzupełniania". Te luki w zabezpieczeniach umożliwiają osobie atakującej odszyfrowanie danych zaszyfrowanych za pomocą algorytmów bloków symetrycznych, takich jak AES i 3DES, przy użyciu nie więcej niż 4096 prób na blok danych. Te luki w zabezpieczeniach korzystają z faktu, że szyfry blokowe są najczęściej używane z weryfikowalnymi danymi uzupełniania na końcu. Stwierdzono, że jeśli osoba atakująca może zmanipulować szyfrowany tekst i dowiedzieć się, czy naruszenie spowodowało błąd w formacie wypełnienia na końcu, osoba atakująca może odszyfrować dane.

Początkowo praktyczne ataki były oparte na usługach, które zwracają różne kody błędów w zależności od tego, czy dopełnienie było prawidłowe, na przykład ASP.NET lukę w zabezpieczeniach MS10-070. Firma Microsoft uważa jednak, że w praktyce można przeprowadzić podobne ataki, korzystając tylko z różnic w czasie między prawidłowym przetwarzaniem a nieprawidłowym wypełnieniem.

Jeśli schemat szyfrowania wykorzystuje podpis i że weryfikacja podpisu jest wykonywana przy użyciu stałego środowiska uruchomieniowego dla danej długości danych (niezależnie od zawartości), integralność danych można zweryfikować bez emitowania jakichkolwiek informacji do osoby atakującej za pośrednictwem kanału bocznego. Ponieważ sprawdzanie integralności odrzuca wszystkie naruszone komunikaty, zagrożenie wyroczni wypełnienia jest ograniczane.

Wskazówki

Przede wszystkim firma Microsoft zaleca, aby wszelkie dane, które mają poufność, były przesyłane za pośrednictwem Transport Layer Security (TLS), następcy Secure Sockets Layer (SSL).

Następnie przeanalizuj aplikację, aby:

  • Dowiedz się dokładnie, jakie szyfrowanie wykonujesz oraz jakie szyfrowanie jest zapewniane przez platformy i interfejsy API, których używasz.
  • Upewnij się, że każde użycie w każdej warstwie algorytmu szyfrowania bloku symetrycznego , takiego jak AES i 3DES, w trybie CBC obejmuje korzystanie z kontroli integralności danych z kluczem tajnym (podpisu asymetrycznego, klucza HMAC lub zmiany trybu szyfrowania na tryb szyfrowania uwierzytelnionego , takiego jak GCM lub CCM).

Na podstawie bieżących badań ogólnie uważa się, że jeśli kroki uwierzytelniania i szyfrowania są wykonywane niezależnie dla trybów szyfrowania innych niż AE, najlepszym rozwiązaniem jest uwierzytelnianie szyfru (szyfrowanie, a następnie podpisywanie). Nie ma jednak jednej uniwersalnej poprawnej odpowiedzi na kryptografię, a to uogólnienie nie jest tak dobre, jak porady kierowane przez profesjonalnego kryptograficznego.

Aplikacje, które nie mogą zmienić formatu wiadomości, ale wykonują nieuwierzyzynione odszyfrowywanie CBC, są zachęcane do próby zastosowania środków zaradczych, takich jak:

  • Odszyfruj bez zezwalania odszyfrowującemu weryfikowanie lub usuwanie wypełnienia:
    • Wszelkie zastosowane uzupełnienia nadal muszą zostać usunięte lub zignorowane, przenosisz obciążenie do aplikacji.
    • Zaletą jest to, że weryfikację i usuwanie wypełnienia można włączyć do innej logiki weryfikacji danych aplikacji. Jeśli weryfikacja wypełnienia i weryfikacja danych mogą być wykonywane w stałym czasie, zagrożenie zostanie ograniczone.
    • Ponieważ interpretacja wypełnienia zmienia postrzeganą długość komunikatu, w tym podejściu mogą nadal być emitowane informacje o chronometrażu.
  • Zmień tryb dopełnienia odszyfrowywania na ISO10126:
    • Wypełnienie odszyfrowywania ISO10126 jest zgodne zarówno z wypełnieniem szyfrowania PKCS7, jak i wypełnieniem szyfrowania ANSIX923.
    • Zmiana trybu zmniejsza wiedzę o wyroczni uzupełniania do 1 bajtu zamiast całego bloku. Jeśli jednak zawartość ma dobrze znaną stopkę, taką jak zamykający element XML, powiązane ataki mogą nadal atakować pozostałą część komunikatu.
    • Nie zapobiega to również odzyskiwaniu w postaci zwykłego tekstu w sytuacjach, w których osoba atakująca może wielokrotnie zaszyfrować ten sam tekst przy użyciu innego przesunięcia komunikatu.
  • Brama oceny wywołania odszyfrowywania w celu odszyfrowania sygnału chronometrażu:
    • Obliczanie czasu przechowywania musi przekraczać maksymalny czas operacji odszyfrowywania dla każdego segmentu danych, który zawiera dopełnienie.
    • Obliczenia czasu powinny być wykonywane zgodnie ze wskazówkami w temacie Uzyskiwanie sygnatur czasowych wysokiej rozdzielczości, a nie przy użyciu (z zastrzeżeniem przerzucania/przepełnienia) lub odejmowanie dwóch systemowych znaczników czasu (z zastrzeżeniem błędów korekty NTP).
    • Obliczenia czasu muszą zawierać operację odszyfrowywania, w tym wszystkie potencjalne wyjątki w zarządzanych aplikacjach lub aplikacjach C++, a nie tylko dopasowywać na końcu.
    • Jeśli powodzenie lub niepowodzenie zostało jeszcze określone, brama chronometrażu musi zwrócić błąd po wygaśnięciu.
  • Usługi wykonujące odszyfrowywanie nieuwierzynione powinny mieć dostępne monitorowanie w celu wykrycia, że przelała się "nieprawidłowa" wiadomość.
    • Należy pamiętać, że ten sygnał zawiera zarówno wyniki fałszywie dodatnie (legalnie uszkodzone dane), jak i wyniki fałszywie ujemne (rozłożenie ataku na wystarczająco długi czas, aby uniknąć wykrycia).

Znajdowanie kodu podatnego na zagrożenia — aplikacje natywne

W przypadku programów budowanych Windows kryptografii: biblioteka nowej generacji (CNG):

  • Wywołanie odszyfrowywania jest do BCryptDecrypt, określając flagę.
  • Dojście klucza zostało zainicjowane przez wywołanie funkcji BCryptSetPropertyz BCRYPT_CHAINING_MODE ustawioną na .
    • Ponieważ BCRYPT_CHAIN_MODE_CBC jest wartością domyślną, kod, którego dotyczy problem, mógł nie mieć przypisanej żadnej wartości dla .BCRYPT_CHAINING_MODE

W przypadku programów budowaną przy użyciu starszej Windows api kryptograficznego:

  • Wywołanie odszyfrowywania jest do CryptDecrypt za pomocą .
  • Dojście klucza zostało zainicjowane przez wywołanie funkcji CryptSetKeyParamz KP_MODE na .
    • Ponieważ CRYPT_MODE_CBC jest wartością domyślną, kod, którego dotyczy problem, mógł nie mieć przypisanej żadnej wartości dla .KP_MODE

Znajdowanie kodu podatnego na zagrożenia — aplikacje zarządzane

Znajdowanie kodu podatnego na zagrożenia — składnia komunikatów kryptograficznych

Nieuwierzytany komunikat CMS EnvelopedData, którego zaszyfrowana zawartość używa trybu 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 (3DES 1.2.840.113549.3.7) lub RC2 (1.2.840.113549.3.2) jest zagrożone, a także komunikaty korzystające z innych algorytmów szyfrowania blokowego w trybie CBC.

Szyfry strumienia nie są podatne na tę określonej luki w zabezpieczeniach, jednak firma Microsoft zaleca, aby zawsze uwierzytelniać dane przez inspekcję wartości ContentEncryptionAlgorithm.

W przypadku aplikacji zarządzanych obiekt blob EnvelopedData usługi CMS można wykryć jako dowolną wartość przekazywaną do usługi System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

W przypadku aplikacji natywnych obiekt blob CMS EnvelopedData można wykryć jako dowolną wartość podaną do dojścia CMS za pośrednictwem polecenia CryptMsgUpdate , którego wynikowy CMSG_TYPE_PARAM to i/lub dojście CMS jest później wysyłane za pośrednictwem polecenia CryptMsgControl.

Przykładowy kod narażony na zagrożenia — zarządzany

Ta metoda odczytuje plik cookie i odszyfrowuje go, a sprawdzanie integralności danych nie jest widoczne. W związku z tym zawartość pliku cookie odczytywana przez tę metodę może zostać zaatakowana przez użytkownika, który go odebrał, lub przez dowolną atakującą, która uzyskała zaszyfrowaną wartość pliku 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();
    }
}

Poniższy przykładowy kod używa niestandardowych formatów komunikatów:

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

gdzie identyfikatory cipher_algorithm_id algorytmów i hmac_algorithm_id są reprezentacjami algorytmów lokalnych aplikacji (niestandardowych). Te identyfikatory mogą mieć sens w innych częściach istniejącego protokołu obsługi komunikatów, a nie jako nagie, zeskonane strumienia bajtowego.

W tym przykładzie użyto również pojedynczego klucza głównego, aby uzyskać zarówno klucz szyfrowania, jak i klucz HMAC. Jest to zarówno udogodnienie dla przekształcania aplikacji z dwoma kluczami w aplikację z podwójnym kluczem, jak i jako zachęta do przechowywania tych dwóch kluczy jako różnych wartości. Gwarantuje to również, że klucz HMAC i klucz szyfrowania nie mogą wyyjść z synchronizacji.

Ten przykład nie akceptuje szyfrowania ani Stream odszyfrowywania. Bieżący format danych sprawia, że szyfrowanie jednowypuste jest trudne, hmac_tag ponieważ wartość poprzedza szyfr. Jednak ten format został wybrany, ponieważ zachowuje wszystkie elementy o stałym rozmiarze na początku, aby zachować prostszą analizę. W przypadku tego formatu danych możliwe jest odszyfrowywanie z jednym przebiegiem, chociaż informatycy ostrzegają o wywołaniu funkcji GetHashAndReset i zweryfikowaniu wyniku przed wywołaniem funkcji TransformFinalBlock. Jeśli szyfrowanie przesyłania strumieniowego jest ważne, może być wymagany inny tryb 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 ciphertest 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;
        }
    }
}

Zobacz też