Megosztás a következőn keresztül:


A CBC módú szimmetrikus visszafejtéssel kapcsolatos biztonsági rések időzítése padding használatával

A Microsoft úgy véli, hogy már nem biztonságos a szimmetrikus titkosítás titkosítási móddal titkosított adatok visszafejtése, ha ellenőrizhető párnázást alkalmaztak anélkül, hogy először biztosították volna a titkosítás integritását, kivéve a nagyon specifikus körülményeket. Ez az ítélet a jelenleg ismert titkosítási kutatásokon alapul.

Bevezetés

A párnázásos oracle-támadás a titkosított adatok elleni támadás típusa, amely lehetővé teszi a támadó számára az adatok tartalmának visszafejtését a kulcs ismerete nélkül.

Az oracle egy "üzenetre" hivatkozik, amely információt ad a támadónak arról, hogy a futtatott művelet helyes-e vagy sem. Képzelje el, hogy egy gyerekkel társasjátékot vagy kártyajátékot játszik. Amikor az arcuk nagy mosollyal világít, mert azt hiszik, hogy jó lépésre készülnek, az egy oracle. Ön, mint az ellenfél, használhatja ezt az orákulumot a következő lépés megfelelő megtervezéséhez.

A padding egy adott titkosítási kifejezés. Egyes titkosítási algoritmusok , amelyek az adatok titkosítására szolgálnak, olyan adatblokkokon dolgoznak, ahol az egyes blokkok rögzített méretűek. Ha a titkosítani kívánt adatok nem a megfelelő méretűek a blokkok kitöltéséhez, az adatok ki lesznek töltve, amíg meg nem történik. A padding számos formája megköveteli, hogy a padding mindig jelen legyen, még akkor is, ha az eredeti bemenet megfelelő méretű volt. Ez lehetővé teszi, hogy a párnázás mindig biztonságosan eltávolítható legyen visszafejtéskor.

A két dolog összevonásával a szoftver implementációja egy párnázott oracle segítségével megmutatja, hogy a visszafejtött adatok rendelkezik-e érvényes kitöltéssel. Az oracle lehet valami olyan egyszerű, mint visszaadni egy értéket, amely azt mondja: "Érvénytelen kitöltés", vagy valami bonyolultabb, mint vesz egy mérhetően eltérő időt feldolgozni egy érvényes blokk, szemben egy érvénytelen blokk.

A blokkalapú titkosításoknak van egy másik tulajdonsága, az úgynevezett mód, amely meghatározza az első blokkban lévő adatok és a második blokk adatai közötti kapcsolatot, és így tovább. Az egyik leggyakrabban használt mód a CBC. A CBC egy kezdeti véletlenszerű blokkot vezet be, amelyet inicializálási vektornak (IV) nevezünk, és egyesíti az előző blokkot a statikus titkosítás eredményével, így az üzenet ugyanazzal a kulccsal történő titkosítása nem mindig ugyanazt a titkosított kimenetet eredményezi.

A támadó a CBC-adatok struktúrájával együtt használhatja a párnázott oracle-t, hogy kissé módosított üzeneteket küldjön az oracle-t elérhetővé tevő kódnak, és addig küldjön adatokat, amíg az oracle nem tájékoztatja őket az adatok helyességéről. Ebből a válaszból a támadó bájtonként visszafejtheti az üzenetet.

A modern számítógépes hálózatok olyan kiváló minőségűek, hogy a támadók nagyon kicsi (0,1 ms-nál kisebb) különbségeket képesek észlelni a távoli rendszerek végrehajtási idejének tekintetében. Azok az alkalmazások, amelyek feltételezik, hogy sikeres visszafejtés csak akkor fordulhat elő, ha az adatokat nem módosították, sebezhetők lehetnek a sikeres és sikertelen visszafejtés különbségeinek megfigyelésére tervezett eszközök támadásaival szemben. Bár ez az időzítési különbség egyes nyelvekben vagy kódtárakban lényegesebb lehet, mint mások, úgy vélik, hogy ez gyakorlati fenyegetés minden nyelvre és kódtárra, ha figyelembe vesszük az alkalmazás sikertelenségre adott válaszát.

Ez a támadás a titkosított adatok módosítására és az eredmény oracle általi tesztelésére támaszkodik. A támadás teljes mérséklésének egyetlen módja a titkosított adatok módosításainak észlelése és a rajta végzett műveletek elutasítása. Ennek szokásos módja, ha létrehoz egy aláírást az adatokhoz, és ellenőrzi az aláírást a műveletek végrehajtása előtt. Az aláírásnak ellenőrizhetőnek kell lennie, a támadó nem hozhatja létre, ellenkező esetben módosítanák a titkosított adatokat, majd kiszámítanak egy új aláírást a módosított adatok alapján. A megfelelő aláírás egyik gyakori típusa a kulcsalapú kivonatoló üzenethitelesítési kód (HMAC). A HMAC abban különbözik az ellenőrzőösszegtől, hogy egy titkos kulcsot használ, amelyet csak a HMAC létrehozója és az azt érvényesítő személy ismer. A kulcs birtoklása nélkül nem tud megfelelő HMAC-t létrehozni. Amikor megkapja az adatokat, a titkosított adatokat veszi fel, egymástól függetlenül számítja ki a HMAC-t az Ön és a küldő megosztás titkos kulcsával, majd összehasonlítja az általuk küldött HMAC-t a kiszámított adatokkal. Ennek az összehasonlításnak állandó időnek kell lennie, ellenkező esetben egy másik észlelhető oracle-t adott hozzá, amely más típusú támadást tesz lehetővé.

Összefoglalva, a párnázott CBC-blokkok titkosításának biztonságos használatához kombinálnia kell őket egy HMAC-val (vagy egy másik adatintegritási ellenőrzéssel), amelyet az adatok visszafejtése előtt állandó idő összehasonlítással ellenőriz. Mivel az összes módosított üzenet ugyanannyi időt vesz igénybe a válasz létrehozásához, a támadás megelőzhető.

Ki sebezhető?

Ez a biztonsági rés a saját titkosítást és visszafejtést végző felügyelt és natív alkalmazásokra is vonatkozik. Ide tartoznak például a következők:

  • Egy alkalmazás, amely titkosít egy cookie-t a kiszolgáló későbbi visszafejtéséhez.
  • Egy adatbázis-alkalmazás, amely lehetővé teszi, hogy a felhasználók adatokat szúrjanak be egy olyan táblába, amelynek oszlopait később visszafejtik.
  • Egy adatátviteli alkalmazás, amely titkosításra támaszkodik egy megosztott kulccsal az átvitt adatok védelme érdekében.
  • Egy alkalmazás, amely "belül" titkosítja és visszafejti az üzeneteket a TLS-alagútban.

Vegye figyelembe, hogy a TLS önmagában nem biztos, hogy védelmet nyújt ezekben a helyzetekben.

Egy sebezhető alkalmazás:

  • Az adatok visszafejtése a CBC titkosítási móddal ellenőrizhető párnázási móddal, például PKCS#7 vagy ANSI X.923 használatával.
  • A visszafejtést adatintegritási ellenőrzés nélkül hajtja végre (MAC vagy aszimmetrikus digitális aláírás használatával).

Ez az absztrakciókra épülő alkalmazásokra is vonatkozik, például a titkosítási üzenet szintaxisára (PKCS#7/CMS) – EnvelopedData struktúra.

A kutatás arra késztette a Microsoftot, hogy tovább foglalkozzon az ISO 10126-tal egyenértékű, ISO 10126-tal egyenértékű kitöltéssel ellátott CBC-üzenetekkel, ha az üzenet jól ismert vagy kiszámítható láblécszerkezettel rendelkezik. Például a W3C XML-titkosítási szintaxisa és feldolgozási ajánlása (xmlenc, EncryptedXml) szabályai szerint előkészített tartalom. Bár a W3C útmutatója az üzenet aláírásához, majd a titkosításhoz akkor megfelelőnek minősült, a Microsoft most azt javasolja, hogy mindig titkosítás után végezze el az aláírást.

Az alkalmazásfejlesztőknek mindig szem előtt kell tartaniuk az aszimmetrikus aláírási kulcs alkalmazhatóságának ellenőrzését, mivel az aszimmetrikus kulcs és egy tetszőleges üzenet között nincs eredendő megbízhatósági kapcsolat.

Részletek

Korábban egyetértés volt abban, hogy fontos a fontos adatok titkosítása és hitelesítése is, például HMAC- vagy RSA-aláírások használatával. A titkosítási és hitelesítési műveletek sorrendbe állítva azonban kevésbé egyértelmű útmutatást kaptak. A cikkben részletezett biztonsági rés miatt a Microsoft útmutatása most az, hogy mindig a "titkosítás és aláírás" paradigmát használja. Vagyis először egy szimmetrikus kulccsal titkosítja az adatokat, majd egy MAC- vagy aszimmetrikus aláírást számít ki a rejtjelszövegen (titkosított adatok). Az adatok visszafejtésekor hajtsa végre a fordított műveletet. Először erősítse meg a MAC-et vagy a titkosítási szöveg aláírását, majd fejtse vissza.

Az "orákulumtámadások" néven ismert biztonsági rések egy osztálya már több mint 10 éve létezik. Ezek a biztonsági rések lehetővé teszik a támadók számára, hogy szimmetrikus blokk-algoritmusokkal, például az AES-sel és a 3DES-sel titkosított adatokat visszafejtse, és blokkonként legfeljebb 4096 kísérletet használjanak. Ezek a biztonsági rések azt a tényt használják, hogy a blokk-titkosításokat leggyakrabban ellenőrizhető kitöltési adatokkal használják a végén. Azt találták, hogy ha egy támadó módosíthatja a rejtjelszöveget, és megtudja, hogy az illetéktelen módosítás hibát okozott-e a kitöltés formátumában a végén, a támadó vissza tudja-e fejteni az adatokat.

Kezdetben a gyakorlati támadások olyan szolgáltatásokon alapultak, amelyek különböző hibakódokat adnának vissza attól függően, hogy a kitöltés érvényes volt-e, például az MS10-070 ASP.NET biztonsági rés. A Microsoft azonban most már úgy véli, hogy célszerű hasonló támadásokat végrehajtani csak az érvényes és érvénytelen kitöltések feldolgozása közötti időzítésbeli különbségekkel.

Feltéve, hogy a titkosítási séma aláírást alkalmaz, és az aláírás-ellenőrzést rögzített futtatókörnyezettel végzik el egy adott adathosszon (a tartalomtól függetlenül), az adatintegritás ellenőrizhető anélkül, hogy bármilyen információt kibocsátanak a támadónak egy oldalsó csatornán keresztül. Mivel az integritás-ellenőrzés elutasítja az illetéktelen üzeneteket, a kipárnázott oracle-fenyegetés enyhíthető.

Útmutató

A Microsoft mindenekelőtt azt javasolja, hogy a bizalmas adatokat a Secure Sockets Layer (SSL) utódjaként, a Transport Layer Securityen (TLS) keresztül kell továbbítani.

Ezután elemezze az alkalmazást a következőre:

  • Pontosan megismerheti, hogy milyen titkosítást végez, és milyen titkosítást biztosít a használt platformok és API-k.
  • Győződjön meg arról, hogy a szimmetrikus blokkok titkosítási algoritmusának minden rétegében ( például az AES és a 3DES) CBC módban való használata magában foglalja a titkos kulcsú adatintegritási ellenőrzés (aszimmetrikus aláírás, HMAC) használatát, vagy a titkosítási módot hitelesített titkosítási (AE) módra( például GCM vagy CCM) kell módosítani.

A jelenlegi kutatások alapján úgy vélik, hogy ha a hitelesítési és titkosítási lépéseket egymástól függetlenül hajtják végre nem AE titkosítási módokon, a titkosítási szöveg hitelesítése (titkosítás- majd jel) a legjobb általános lehetőség. A titkosításra azonban nincs egy méretre illeszkedő helyes válasz, és ez az általánosítás nem olyan jó, mint egy profi kriptográfus útmutatása.

Azokat az alkalmazásokat, amelyek nem tudják módosítani az üzenetkezelési formátumukat, de nem hitelesített CBC-visszafejtést hajtanak végre, javasoljuk, hogy próbálkozzon a következő megoldásokkal:

  • Visszafejtés anélkül, hogy lehetővé tenné a visszafejtő számára a padding ellenőrzését vagy eltávolítását:
    • Az alkalmazott kitöltéseket továbbra is el kell távolítani vagy figyelmen kívül kell hagyni, a terhet áthelyezi az alkalmazásba.
    • Ennek előnye, hogy a párnázás ellenőrzése és eltávolítása beépíthető más alkalmazásadat-ellenőrzési logikába. Ha a párnázás ellenőrzése és az adatok ellenőrzése állandó időben elvégezhető, a fenyegetés csökken.
    • Mivel a kitöltés értelmezése megváltoztatja az észlelt üzenet hosszát, előfordulhat, hogy ennek a megközelítésnek az időzítési információi is megjelennek.
  • Módosítsa a visszafejtési mód ISO10126:
    • ISO10126 visszafejtési párnázás kompatibilis a PKCS7 titkosítási párnázással és ANSIX923 titkosítási párnázással.
    • A mód módosítása a teljes blokk helyett 1 bájtra csökkenti a párnázási oracle-tudást. Ha azonban a tartalomnak van egy jól ismert lábléce, például egy záró XML-elem, a kapcsolódó támadások továbbra is megtámadhatják az üzenet többi részét.
    • Ez nem akadályozza meg az egyszerű szöveges helyreállítást olyan helyzetekben, amikor a támadó ugyanazt a egyszerű szöveget többször is titkosíthatja egy másik üzeneteltolással.
  • A visszafejtési hívás kiértékelésének kapuja az időzítési jel csillapításához:
    • A visszatartási idő számításának minimálisan meg kell haladhatja azt a maximális időtartamot, amelyet a visszafejtési művelet a paddingot tartalmazó adatszegmensek esetében igénybe vehet.
    • Az időszámításokat a nagy felbontású időbélyegek beszerzésére vonatkozó útmutatásnak megfelelően kell elvégezni, nem pedig a (túlgördítési/túlcsordulási) vagy Environment.TickCount két rendszeridőbélyeg kivonásával (az NTP-beállítási hibáktól függően).
    • Az időszámításnak tartalmaznia kell a visszafejtési műveletet, beleértve a felügyelt vagy C++ alkalmazások összes lehetséges kivételét, és nem csak a végére kell illeszteni.
    • Ha a sikeresség vagy a hiba még nincs meghatározva, az időzítési kapunak vissza kell adnia a hibát, amikor lejár.
  • A hitelesítés nélküli visszafejtést végző szolgáltatásoknak monitorozással kell rendelkezniük, hogy észleljék, hogy "érvénytelen" üzenetek áradtak át.
    • Ne feledje, hogy ez a jel hamis pozitív (jogszerűen sérült adatok) és hamis negatívumokat is hordoz (a támadást elég hosszú ideig terjeszti, hogy elkerülje az észlelést).

Sebezhető kód keresése – natív alkalmazások

A Windows Cryptography: Next Generation (CNG) kódtárhoz készült programok esetén:

  • A visszafejtési hívás a BCryptDecrypt, amely megadja a jelölőtBCRYPT_BLOCK_PADDING.
  • A kulcsfogópont inicializálása a BCryptSetProperty meghívásával történt, és a BCRYPT_CHAINING_MODE értékre BCRYPT_CHAIN_MODE_CBCvan állítva.
    • Mivel BCRYPT_CHAIN_MODE_CBC ez az alapértelmezett érték, előfordulhat, hogy az érintett kód nem rendelt hozzá értéket.BCRYPT_CHAINING_MODE

A régebbi Windows Cryptographic API-ra épülő programok esetén:

  • A visszafejtési hívás a CryptDecrypt with Final=TRUE.
  • A kulcsfogópont inicializálása a CryptSetKeyParam meghívásával történt, és a KP_MODE értékre CRYPT_MODE_CBCvan állítva.
    • Mivel CRYPT_MODE_CBC ez az alapértelmezett érték, előfordulhat, hogy az érintett kód nem rendelt hozzá értéket.KP_MODE

Sebezhető kód keresése – felügyelt alkalmazások

Sebezhető kód keresése – titkosítási üzenet szintaxisa

Nem hitelesített CMS EnvelopedData-üzenet, amelynek titkosított tartalma az AES CBC-módját használja (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) vagy RC2 (1.2.840.113549.3.2) sebezhető, valamint a CBC módban más blokkfelfedő algoritmusokat használó üzeneteket is.

Bár a stream-titkosítások nem érzékenyek erre a biztonsági résre, a Microsoft azt javasolja, hogy mindig hitelesítse az adatokat a ContentEncryptionAlgorithm érték vizsgálatával.

Felügyelt alkalmazások esetén a CMS EnvelopedData-blob bármely olyan értékként észlelhető, amelyet a rendszer átad a rendszernek System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

Natív alkalmazások esetén a CMS EnvelopedData-blobok a CryptMsgUpdate-en keresztül a CMS-leírókhoz megadott bármely értékként észlelhetők, amelynek eredménye CMSG_TYPE_PARAM ésCMSG_ENVELOPED/vagy a CMS-leíró később utasítást CMSG_CTRL_DECRYPT kap a CryptMsgControlon keresztül.

Sebezhető kód példa – felügyelt

Ez a módszer beolvassa a cookie-kat, és visszafejti azt, és nem látható az adatintegritási ellenőrzés. Ezért az e módszer által beolvasott cookie-k tartalmát megtámadhatja az azt megkapó felhasználó, vagy bármely támadó, aki megszerezte a titkosított cookie-értéket.

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

Az alábbi mintakód nem szabványos üzenetformátumot használ:

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

ahol az és az cipher_algorithm_idhmac_algorithm_id algoritmus-azonosítók alkalmazáson belüli (nem szabványos) ábrázolásai ezeknek az algoritmusoknak. Ezek az azonosítók a meglévő üzenetkezelési protokoll más részeiben is értelmezhetők, nem pedig csupasz összefűzött bájtfolyamként.

Ez a példa egyetlen főkulcsot is használ egy titkosítási kulcs és egy HMAC-kulcs származtatásához. Ez kényelmesen használható a kétkulcsos alkalmazások kétkulcsossá alakításához, valamint a két kulcs különböző értékekként való megtartásához. Emellett garantálja, hogy a HMAC-kulcs és a titkosítási kulcs nem tud kijönni a szinkronizálásból.

Ez a minta nem fogad el titkosítást Stream vagy visszafejtést. Az aktuális adatformátum megnehezíti az egyátengéses titkosítást, mert az hmac_tag érték megelőzi a rejtjelszöveget. Ez a formátum azonban azért lett kiválasztva, mert az összes rögzített méretű elemet az elején tartja, hogy az elemző egyszerűbb legyen. Ez az adatformátum lehetővé teszi az egyátengetéses visszafejtést, bár a implementátort figyelmezteti, hogy hívja meg a GetHashAndReset, és ellenőrizze az eredményt a TransformFinalBlock meghívása előtt. Ha fontos a streamtitkosítás, előfordulhat, hogy más AE-módra van szükség.

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

Lásd még