Dela via


Sårbarheter vid tidpunkter med symmetrisk dekryptering i CBC-läge med hjälp av utfyllnad

Microsoft anser att det inte längre är säkert att dekryptera data som krypterats med CBC-läget (Cipher-Block-Chaining) när verifierbar utfyllnad har tillämpats utan att först säkerställa chiffertextens integritet, med undantag för mycket specifika omständigheter. Denna bedömning baseras på för närvarande känd kryptografisk forskning.

Introduktion

Ett utfyllnadsorakelangrepp är en typ av attack mot krypterade data som gör att angriparen kan dekryptera innehållet i data, utan att känna till nyckeln.

Ett orakel refererar till en "tell" som ger en angripare information om huruvida åtgärden de utför är korrekt eller inte. Föreställ dig att spela ett bräd- eller kortspel med ett barn. När deras ansikte lyser upp med ett stort leende eftersom de tror att de är på väg att göra ett bra drag, är det ett orakel. Du kan som motståndare använda det här oraklet för att planera nästa steg på rätt sätt.

Utfyllnad är en specifik kryptografisk term. Vissa chiffer, som är de algoritmer som används för att kryptera dina data, fungerar på datablock där varje block har en fast storlek. Om de data som du vill kryptera inte är rätt storlek för att fylla blocken, är dina data vadderade tills de gör det. Många former av utfyllnad kräver att utfyllnad alltid finns, även om den ursprungliga indatan var av rätt storlek. På så sätt kan utfyllnad alltid tas bort på ett säkert sätt vid dekryptering.

En programvaruimplementering med ett utfyllnadsorakel visar om dekrypterade data har giltig utfyllnad. Oraklet kan vara något så enkelt som att returnera ett värde som säger "Ogiltig utfyllnad" eller något mer komplicerat som att ta en mätbart annorlunda tid för att bearbeta ett giltigt block i stället för ett ogiltigt block.

Blockbaserade chiffer har en annan egenskap, som kallas läge, som avgör relationen mellan data i det första blocket och data i det andra blocket och så vidare. Ett av de vanligaste lägena är CBC. CBC introducerar ett första slumpmässigt block, som kallas initieringsvektor (IV), och kombinerar det tidigare blocket med resultatet av statisk kryptering så att kryptering av samma meddelande med samma nyckel inte alltid ger samma krypterade utdata.

En angripare kan använda ett utfyllnadsorakel, i kombination med hur CBC-data är strukturerade, för att skicka något ändrade meddelanden till koden som exponerar oraklet och fortsätta skicka data tills oraklet säger att data är korrekta. Från det här svaret kan angriparen dekryptera meddelandebytet med byte.

Moderna datornätverk är av så hög kvalitet att en angripare kan upptäcka mycket små (mindre än 0,1 ms) skillnader i körningstiden på fjärrsystem. Program som antar att en lyckad dekryptering bara kan inträffa när data inte manipulerades kan vara sårbara för angrepp från verktyg som är utformade för att observera skillnader i lyckad och misslyckad dekryptering. Även om den här tidsskillnaden kan vara mer betydande i vissa språk eller bibliotek än andra, tror man nu att detta är ett praktiskt hot för alla språk och bibliotek när programmets svar på fel beaktas.

Den här attacken bygger på möjligheten att ändra krypterade data och testa resultatet med oraklet. Det enda sättet att helt minimera attacken är att identifiera ändringar i krypterade data och vägra att utföra några åtgärder på dem. Standardsättet är att skapa en signatur för data och verifiera signaturen innan några åtgärder utförs. Signaturen måste vara verifierbar, den kan inte skapas av angriparen, annars ändrar de krypterade data och beräknar sedan en ny signatur baserat på ändrade data. En vanlig typ av lämplig signatur kallas för en nyckel-hash-meddelandeautentiseringskod (HMAC). En HMAC skiljer sig från en kontrollsumma eftersom den tar en hemlig nyckel, känd endast för den person som producerar HMAC och till den person som validerar den. Utan nyckelinnehav kan du inte skapa en korrekt HMAC. När du tar emot dina data tar du krypterade data, beräknar HMAC oberoende med hjälp av den hemliga nyckeln du och avsändarresursen och jämför sedan den HMAC som de skickade mot den du beräknade. Den här jämförelsen måste vara konstant, annars har du lagt till ett annat påvisbart orakel som tillåter en annan typ av angrepp.

Sammanfattningsvis, om du vill använda vadderade CBC-block chiffer på ett säkert sätt, måste du kombinera dem med en HMAC (eller en annan dataintegritetskontroll) som du verifierar med en konstant tidsjämförelse innan du försöker dekryptera data. Eftersom alla ändrade meddelanden tar samma tid att generera ett svar förhindras attacken.

Vem är sårbart

Den här sårbarheten gäller både hanterade och inbyggda program som utför sin egen kryptering och dekryptering. Detta omfattar till exempel:

  • Ett program som krypterar en cookie för senare dekryptering på servern.
  • Ett databasprogram som ger användarna möjlighet att infoga data i en tabell vars kolumner senare dekrypteras.
  • Ett dataöverföringsprogram som förlitar sig på kryptering med hjälp av en delad nyckel för att skydda data under överföring.
  • Ett program som krypterar och dekrypterar meddelanden "inuti" TLS-tunneln.

Observera att det inte går att skydda dig i dessa scenarier med enbart TLS.

Ett sårbart program:

  • Dekrypterar data med CBC-chifferläget med ett verifierbart utfyllnadsläge, till exempel PKCS#7 eller ANSI X.923.
  • Utför dekrypteringen utan att ha utfört en dataintegritetskontroll (via en MAC eller en asymmetrisk digital signatur).

Detta gäller även för program som bygger på abstraktioner utöver dessa primitiver, till exempel kryptografisk meddelandesyntax (PKCS#7/CMS) EnvelopedData-strukturen.

Forskning har lett till att Microsoft är mer oroat över CBC-meddelanden som är vadderade med ISO 10126-motsvarande utfyllnad när meddelandet har en välkänd eller förutsägbar sidfotsstruktur. Till exempel innehåll som förbereds enligt reglerna för W3C XML-krypteringssyntax och bearbetningsrekommendering (xmlenc, EncryptedXml). Medan W3C-vägledningen för att signera meddelandet och sedan kryptera ansågs lämplig vid den tidpunkten rekommenderar Microsoft nu att du alltid gör krypteringstecken.

Programutvecklare bör alltid vara medvetna om att verifiera tillämpligheten för en asymmetrisk signaturnyckel, eftersom det inte finns någon inbyggd förtroenderelation mellan en asymmetrisk nyckel och ett godtyckligt meddelande.

Details

Historiskt sett har det funnits konsensus om att det är viktigt att både kryptera och autentisera viktiga data med hjälp av metoder som HMAC- eller RSA-signaturer. Det har dock funnits mindre tydliga riktlinjer för hur krypterings- och autentiseringsåtgärderna ska sekvensiseras. På grund av den sårbarhet som beskrivs i den här artikeln är Microsofts vägledning nu att alltid använda paradigmet "encrypt-then-sign". Det vill: först kryptera data med hjälp av en symmetrisk nyckel och sedan beräkna en MAC eller asymmetrisk signatur över chiffertexten (krypterade data). När du dekrypterar data utför du det omvända. Bekräfta först MAC eller signaturen för chiffertexten och dekryptera den sedan.

En klass av sårbarheter som kallas "utfyllnad av oracle-attacker" har funnits i över 10 år. Dessa sårbarheter gör det möjligt för en angripare att dekryptera data som krypterats av symmetriska blockalgoritmer, till exempel AES och 3DES, med högst 4 096 försök per datablock. Dessa sårbarheter använder det faktum att block chiffer används oftast med verifierbara utfyllnadsdata i slutet. Det konstaterades att om en angripare kan manipulera chiffertexten och ta reda på om manipuleringen orsakade ett fel i formatet för utfyllnad i slutet, kan angriparen dekryptera data.

Inledningsvis baserades praktiska attacker på tjänster som skulle returnera olika felkoder baserat på om utfyllnad var giltig, till exempel ASP.NET sårbarhet MS10-070. Microsoft anser dock nu att det är praktiskt att utföra liknande attacker med bara skillnaderna i tidsinställning mellan bearbetningens giltiga och ogiltiga utfyllnad.

Förutsatt att krypteringsschemat använder en signatur och att signaturverifieringen utförs med en fast körning för en viss datalängd (oavsett innehållet) kan dataintegriteten verifieras utan att någon information skickas till en angripare via en sidokanal. Eftersom integritetskontrollen avvisar alla manipulerade meddelanden minimeras utfyllnadshotet.

Vägledning

Först och främst rekommenderar Microsoft att alla data som har sekretess måste överföras via TLS (Transport Layer Security), efterföljare till Secure Sockets Layer (SSL).

Analysera sedan ditt program för att:

  • Förstå exakt vilken kryptering du utför och vilken kryptering som tillhandahålls av de plattformar och API:er som du använder.
  • Var säker på att varje användning på varje lager av en symmetrisk blockkrypteringsalgoritm, till exempel AES och 3DES, i CBC-läge innefattar användning av en hemlig nyckelbaserad dataintegritetskontroll (en asymmetrisk signatur, en HMAC eller för att ändra chifferläget till ett autentiserat krypteringsläge (AE), till exempel GCM eller CCM).

Baserat på den aktuella forskningen är det allmänt trott att när autentiserings- och krypteringsstegen utförs oberoende för icke-AE-krypteringslägen är autentisering av chiffertexten (kryptera-sedan-tecken) det bästa allmänna alternativet. Det finns dock inget rätt svar på kryptografi som passar alla och den här generaliseringen är inte lika bra som riktade råd från en professionell kryptograf.

Program som inte kan ändra meddelandeformatet men utför oautentiserad CBC-dekryptering uppmanas att försöka införliva åtgärder som:

  • Dekryptera utan att tillåta att dekryptatorn verifierar eller tar bort utfyllnad:
    • All utfyllnad som tillämpades måste fortfarande tas bort eller ignoreras. Du flyttar belastningen till ditt program.
    • Fördelen är att utfyllnadsverifieringen och borttagningen kan införlivas i annan logik för verifiering av programdata. Om utfyllnadsverifieringen och dataverifieringen kan utföras i konstant tid minskas hotet.
    • Eftersom tolkningen av utfyllnad ändrar den upplevda meddelandelängden kan det fortfarande finnas tidsinformation som genereras från den här metoden.
  • Ändra utfyllnadsläget för dekryptering till ISO10126:
    • ISO10126 dekrypteringsutfyllnad är kompatibel med både PKCS7-krypteringsutfyllnad och ANSIX923 krypteringsutfyllnad.
    • Om du ändrar läget minskar utfyllnadsorakelkunskapen till 1 byte i stället för hela blocket. Men om innehållet har en välkänd sidfot, till exempel ett avslutande XML-element, kan relaterade attacker fortsätta att attackera resten av meddelandet.
    • Detta förhindrar inte heller återställning i klartext i situationer där angriparen kan tvinga samma klartext att krypteras flera gånger med en annan meddelandeförskjutning.
  • Gate utvärderingen av ett dekrypteringsanrop för att dämpa tidssignalen:
    • Beräkningen av undantagstiden måste ha ett minimum som överstiger den maximala tid som dekrypteringsåtgärden skulle ta för alla datasegment som innehåller utfyllnad.
    • Tidsberäkningar bör göras enligt vägledningen i Hämta tidsstämplar med hög upplösning, inte genom att använda Environment.TickCount (med förbehåll för överrullning/spill) eller subtrahera två systemtidsstämplar (med förbehåll för NTP-justeringsfel).
    • Tidsberäkningar måste inkludera dekrypteringsåtgärden, inklusive alla potentiella undantag i hanterade program eller C++-program, inte bara vadderade i slutet.
    • Om framgång eller fel har fastställts ännu måste tidsgrinden returnera fel när den upphör att gälla.
  • Tjänster som utför oautentiserad dekryptering bör ha övervakning på plats för att upptäcka att en flod av "ogiltiga" meddelanden har kommit igenom.
    • Tänk på att denna signal bär både falska positiva (legitimt skadade data) och falska negativa (sprider ut attacken under tillräckligt lång tid för att undvika identifiering).

Hitta sårbar kod – inbyggda program

För program som skapats mot biblioteket Windows Cryptography: Next Generation (CNG):

  • Dekrypteringsanropet är till BCryptDecrypt och anger BCRYPT_BLOCK_PADDING flaggan.
  • Nyckelhandtaget har initierats genom att anropa BCryptSetProperty med BCRYPT_CHAINING_MODE inställt på BCRYPT_CHAIN_MODE_CBC.
    • Eftersom BCRYPT_CHAIN_MODE_CBC är standardvärdet kanske den berörda koden inte har tilldelat något värde för BCRYPT_CHAINING_MODE.

För program som skapats mot det äldre Kryptografiska API:et för Windows:

  • Dekrypteringsanropet är till CryptDecrypt med Final=TRUE.
  • Nyckelhandtaget har initierats genom att anropa CryptSetKeyParam med KP_MODE inställt på CRYPT_MODE_CBC.
    • Eftersom CRYPT_MODE_CBC är standardvärdet kanske den berörda koden inte har tilldelat något värde för KP_MODE.

Hitta sårbar kod – hanterade program

Hitta sårbar kod – kryptografisk meddelandesyntax

Ett oautentiserat CMS EnvelopedData-meddelande vars krypterade innehåll använder CBC-läget för 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) eller RC2 (1.2.840.113549.3.2) är sårbar, samt meddelanden som använder andra blockkrypteringsalgoritmer i CBC-läge.

Även om ström chiffer inte är mottagliga för just den här sårbarheten rekommenderar Microsoft att du alltid autentiserar data över att inspektera värdet ContentEncryptionAlgorithm.

För hanterade program kan en CMS EnvelopedData-blob identifieras som valfritt värde som skickas till System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

För interna program kan en CMS EnvelopedData-blob identifieras som valfritt värde som tillhandahålls till en CMS-referens via CryptMsgUpdate vars resulterande CMSG_TYPE_PARAM är CMSG_ENVELOPED och/eller cms-handtaget senare skickas en CMSG_CTRL_DECRYPT instruktion via CryptMsgControl.

Exempel på sårbar kod – hanterad

Den här metoden läser en cookie och dekrypterar den och ingen dataintegritetskontroll visas. Innehållet i en cookie som läses med den här metoden kan därför attackeras av användaren som tog emot den, eller av en angripare som har fått det krypterade cookievärdet.

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();
    }
}

Följande exempelkod använder ett meddelandeformat som inte är standard

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

där algoritmidentifierarna cipher_algorithm_id och hmac_algorithm_id är programlokala (icke-standard) representationer av dessa algoritmer. Dessa identifierare kan vara meningsfulla i andra delar av ditt befintliga meddelandeprotokoll i stället för som en bare concatenated bytestream.

I det här exemplet används också en enda huvudnyckel för att härleda både en krypteringsnyckel och en HMAC-nyckel. Detta tillhandahålls både som en bekvämlighet för att omvandla ett singly-keyed-program till ett program med dubbla nycklar och för att uppmuntra till att behålla de två nycklarna som olika värden. Det garanterar dessutom att HMAC-nyckeln och krypteringsnyckeln inte kan komma ur synkroniseringen.

Det här exemplet accepterar inte en Stream för kryptering eller dekryptering. Det aktuella dataformatet gör det svårt att kryptera en enda gång eftersom hmac_tag värdet föregår chiffertexten. Det här formatet valdes dock eftersom det behåller alla element med fast storlek i början för att hålla parsern enklare. Med det här dataformatet är enstaka dekryptering möjlig, även om en implementer uppmanas att anropa GetHashAndReset och verifiera resultatet innan transformFinalBlock anropas. Om strömningskryptering är viktigt kan ett annat AE-läge krävas.

// ==++==
//
//   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;
        }
    }
}

Se även