Zranitelnosti způsobené časováním u symetrického dešifrování v režimu CBC pomocí vycpávky

Společnost Microsoft se domnívá, že není bezpečné dešifrovat data zašifrovaná pomocí režimu cipher-Block-Chaining (CBC), pokud se použije ověřitelné odsazení, aniž by se nejprve zajistila integrita šifrovaného textu, s výjimkou velmi specifických okolností. Tento úsudek vychází z aktuálně známého kryptografického výzkumu.

Úvod

Útok typu padding oracle je typ útoku na šifrovaná data, který útočníkovi umožňuje dešifrovat obsah dat bez znalosti klíče.

Orákulum odkazuje na "řekněte", což útočníkovi poskytne informace o tom, jestli je akce, kterou spouští, správná nebo ne. Představte si, že hrajete hru na desku nebo kartu s dítětem. Když se jejich tvář rozsvítí velkým úsměvem, protože si myslí, že se chystají udělat dobrý tah, to je jako prorok. Vy, jako soupeř, můžete toto orákulum použít k plánování svého dalšího kroku vhodným způsobem.

Vycpávka je konkrétní kryptografický termín. Některé šifry, což jsou algoritmy používané k šifrování dat, pracují na blocích dat, kde je každý blok pevnou velikostí. Pokud data, která chcete šifrovat, nejsou správnou velikostí pro vyplnění bloků, budou data doplněna, dokud nebudou pasovat. Mnoho forem vycpávání vyžaduje, aby vycpávání bylo vždy přítomno, i přesto, že původní vstup měl správnou velikost. To zajišťuje, že při dešifrování může být vycpávka vždy bezpečně odstraněna.

Spojením těchto dvou věcí implementace softwaru s orákulem pro zarovnání odhalí, jestli dešifrovaná data mají platné zarovnání. Orákulum může být tak jednoduché jako vrácení hodnoty s nápisem "Neplatná výplň" nebo něco složitějšího, jako když zpracování platného bloku trvá měřitelně odlišnou dobu oproti blokování neplatného bloku.

Šifry založené na bloku mají další vlastnost, která se nazývá režim, který určuje vztah dat v prvním bloku k datům v druhém bloku atd. Jedním z nejčastěji používaných režimů je CBC. CBC zavádí počáteční náhodný blok, známý jako inicializační vektor (IV), a kombinuje jej s předchozím blokem a výsledkem statického šifrování. Tímto způsobem zajišťuje, že šifrování stejné zprávy se stejným klíčem nevede vždy ke stejnému šifrovanému výstupu.

Útočník může použít padding orákulum v kombinaci se strukturou dat CBC, aby odeslal mírně změněné zprávy do kódu, který odhaluje orákulum, a nadále odesílal data, dokud mu orákulum neřekne, že data jsou správná. Z této odpovědi může útočník dešifrovat zprávu bajt po bajtu.

Moderní počítačové sítě mají takovou vysokou kvalitu, že útočník může ve vzdálených systémech detekovat velmi malé rozdíly (méně než 0,1 ms). Aplikace, které předpokládají, že k úspěšnému dešifrování může dojít pouze v případě, že data nebyla manipulována, mohou být zranitelné vůči útokům z nástrojů navržených tak, aby sledovaly rozdíly v úspěšném a neúspěšném dešifrování. I když tento rozdíl časování může být v některých jazycích nebo knihovnách důležitější než u jiných, předpokládá se, že se jedná o praktickou hrozbu pro všechny jazyky a knihovny, když se bere v úvahu reakce aplikace na selhání.

Tento útok spoléhá na schopnost měnit šifrovaná data a testovat výsledek pomocí orákulumu. Jediným způsobem, jak útok plně zmírnit, je detekovat změny šifrovaných dat a odmítnout s ním provádět jakékoli akce. Standardním způsobem, jak to udělat, je vytvořit podpis pro data a ověřit podpis před provedením jakýchkoli operací. Podpis musí být ověřitelný, útočník ho nemůže vytvořit, jinak by změnil šifrovaná data a pak na základě změněných dat vypočítal nový podpis. Jeden běžný typ vhodného podpisu se označuje jako ověřovací kód HMAC (keyed-hash message authentication code). HMAC se liší od kontrolního součtu v tom, že používá tajný klíč, známý pouze osobě, která vytváří kód HMAC a osobě, která ho ověřuje. Bez vlastnictví klíče nemůžete vytvořit správný kód HMAC. Když obdržíte data, vezmete šifrovaná data, nezávisle vypočítáte HMAC pomocí tajného klíče, který sdílíte s odesílatelem, a pak porovnáte HMAC, který vám poslali, s tím, který jste vypočítali. Toto porovnání musí být konstantního času, jinak jste přidali další detekovatelnou zranitelnost, což umožňuje jiný typ útoku.

Pokud chcete v souhrnu bezpečně používat blokové šifry CBC, musíte je zkombinovat s kontrolou HMAC (nebo jinou kontrolou integrity dat), kterou před pokusem o dešifrování dat ověříte pomocí konstantního porovnání času. Vzhledem k tomu, že všechny změněné zprávy zabírají stejnou dobu, aby vznikla odpověď, zabrání se útoku.

Kdo je zranitelný

Toto ohrožení zabezpečení se týká spravovaných i nativních aplikací, které provádějí vlastní šifrování a dešifrování. Patří sem například:

  • Aplikace, která zašifruje soubor cookie pro pozdější dešifrování na serveru.
  • Databázová aplikace, která uživatelům umožňuje vložit data do tabulky, jejíž sloupce se později dešifrují.
  • Aplikace pro přenos dat, která využívá šifrování pomocí sdíleného klíče k ochraně přenášených dat.
  • Aplikace, která šifruje a dešifruje zprávy "uvnitř" tunelu TLS.

Mějte na paměti, že použití samotného protokolu TLS vás v těchto scénářích nemusí chránit.

Ohrožená aplikace:

  • Dešifruje data pomocí šifrovacího režimu CBC s ověřitelným režimem vycpání, například PKCS#7 nebo ANSI X.923.
  • Provede dešifrování bez provedení kontroly integrity dat (prostřednictvím mac nebo asymetrického digitálního podpisu).

To platí také pro aplikace založené na abstrakcích nad těmito primitivy, jako je kryptografická syntaxe zpráv (PKCS#7/CMS) EnvelopedData struktura.

Výzkum vedl k tomu, aby se společnost Microsoft více obávala zpráv CBC, které jsou vycpané ekvivalentním odsazením podle normy ISO 10126, pokud má zpráva dobře známou nebo předvídatelnou strukturu zápatí. Například obsah připravený podle pravidel doporučení pro šifrování XML a zpracování W3C (xmlenc, EncryptedXml). I když pokyny W3C k podepsání zprávy a poté zašifrování byly tehdy považovány za vhodné, Microsoft teď doporučuje vždy nejdříve zašifrovat a potom podepsat.

Vývojáři aplikací by vždy měli mít na paměti ověření použitelnosti asymetrického podpisového klíče, protože mezi asymetrickým klíčem a libovolnou zprávou neexistuje žádný vztah důvěryhodnosti.

Podrobnosti

V minulosti došlo ke konsensus, že je důležité šifrovat a ověřovat důležitá data pomocí podpisů HMAC nebo RSA. Existují však méně jasné pokyny, jak sekvencovat operace šifrování a ověřování. Vzhledem ke zranitelnosti, která je podrobně popsaná v tomto článku, jsou teď pokyny Microsoftu k tomu, aby se vždy používalo paradigma "encrypt-then-sign". To znamená, že nejprve zašifrujete data pomocí symetrického klíče a poté vypočítáte MAC nebo vytvoříte asymetrický podpis nad šifrovaným textem. Při dešifrování dat proveďte opak. Nejprve potvrďte mac nebo podpis šifrovacího textu a dešifrujte ho.

Již více než 10 let je známo, že existuje třída zranitelností označovaná jako "útoky typu padding oracle". Tato ohrožení zabezpečení umožňují útočníkovi dešifrovat data zašifrovaná algoritmy symetrického bloku, jako je AES a 3DES, a to bez více než 4096 pokusů na jeden blok dat. Tato ohrožení zabezpečení využívají skutečnost, že blokové šifry se nejčastěji používají s ověřitelnými výplňovými údaji na konci. Zjistilo se, že pokud útočník může manipulovat s šifrovaným textem a zjistit, jestli manipulace způsobila chybu ve formátu vycpávky na konci, může útočník data dešifrovat.

Na začátku byly praktické útoky založeny na službách, které vracely různé kódy chyb podle platnosti odsazení, například na zranitelnost ASP.NET MS10-070. Společnost Microsoft se však domnívá, že je praktické provádět podobné útoky pouze pomocí rozdílů v načasování mezi platným zpracováním a neplatným odsazením.

Za předpokladu, že schéma šifrování využívá podpis a že se ověření podpisu provádí s pevným modulem runtime pro danou délku dat (bez ohledu na obsah), je možné integritu dat ověřit bez vyzrazování jakýchkoli informací útočníkovi prostřednictvím bočního kanálu. Vzhledem k tomu, že kontrola integrity odmítne všechny zfalšované zprávy, je hrozba odsazení oracle zmírněna.

Vodítko

Microsoft především doporučuje, aby veškerá data, která mají důvěrnost, byla přenášena přes protokol TLS (Transport Layer Security), následníkem protokolu SSL (Secure Sockets Layer).

Dále analyzujte svou aplikaci, abyste:

  • Porozumíte přesně tomu, jaké šifrování provádíte a jaké šifrování poskytují platformy a rozhraní API, které používáte.
  • Ujistěte se, že každé použití v každé vrstvě symetrického blokového šifrovacího algoritmu, jako je AES a 3DES, v CBC režimu zahrnuje použití kontroly integrity dat s tajně klíčovou (asymetrický podpis, HMAC, nebo změnu režimu šifrování na ověřený režim šifrování (AE), jako je GCM nebo CCM).

Na základě aktuálního výzkumu se obecně předpokládá, že když se ověřovací a šifrovací kroky provádějí nezávisle na režimech šifrování, které nejsou režimy AE, je nejlepší obecnou možností ověřování šifrovacího textu (encrypt-then-sign). Neexistuje však žádná jediná správná odpověď na kryptografii způsobem, který by vyhovoval všem, a tato generalizace není tak hodnotná jako doporučení od profesionálního kryptografa.

Aplikace, které nemůžou změnit formát zasílání zpráv, ale provádějí neověřené dešifrování CBC, se doporučuje, aby se pokusily začlenit zmírnění rizik, například:

  • Dešifrovat bez umožnění, aby dešifrovač ověřoval nebo odstraňoval vyplnění.
    • Veškeré použité odsazení je stále potřeba odebrat nebo ignorovat, protože tím přesouváte zátěž do své aplikace.
    • Výhodou je, že ověření odsazení a odebrání je možné začlenit do jiné logiky ověření dat aplikace. Pokud se ověření vyplnění a ověření dat dá provést v konstantním čase, hrozba se sníží.
    • Vzhledem k tomu, že interpretace vyplnění může změnit vnímanou délku zprávy, mohou být z tohoto přístupu stále získány informace o načasování.
  • Změňte režim vycpávací dešifrování na ISO10126.
    • Formát výplně pro dešifrování ISO10126 je kompatibilní s výplněmi šifrování PKCS7 a ANSIX923.
    • Změna režimu omezuje odhalení útoku typu oracle na 1 bajt místo celého datového bloku. Pokud ale obsah obsahuje dobře známé zápatí, jako je například koncový prvek XML, související útoky mohou nadále útočit na zbytek zprávy.
    • To rovněž nezabrání obnovení otevřeného textu v situacích, kdy útočník může donutit opakovaně zašifrovat stejný otevřený text s různým posunem zprávy.
  • Regulovat vyhodnocení dešifrovacího volání pro snížení časového signálu:
    • Výpočet doby uchování musí být minimálně o tolik, aby převyšovala maximální dobu, po kterou by operace dešifrování trvala pro jakýkoliv datový segment, který obsahuje vycpávku.
    • Časové výpočty by měly být provedeny podle pokynů pro získání časových razítek s vysokým rozlišením, nikoli pomocí (náchylné k převrácení nebo přetečení) nebo odečtením dvou časových razítek systému (kvůli chybám úpravy NTP).
    • Časové výpočty musí zahrnovat operaci dešifrování včetně zpracování všech potenciálních výjimek ve spravovaných aplikacích nebo aplikacích C++, a nesmí být pouze přidávány na konec.
    • Pokud ještě nebyl určen úspěch nebo selhání, časová brána musí po vypršení platnosti vykázat neúspěch.
  • Služby, které provádějí neověřené dešifrování, by měly mít zavedené monitorování, aby bylo zjištěno, že došlo k záplavě "neplatných" zpráv.
    • Mějte na paměti, že tento signál nese falešně pozitivní (legitimní poškozená data) i falešně negativní výsledky (šíření útoku po dostatečně dlouhou dobu, aby se detekce vyhnula).

Vyhledání zranitelného kódu – nativní aplikace

Pro programy vytvořené v knihovně Windows Cryptography: Next Generation (CNG):

  • Volání dešifrování je na BCryptDecrypt, přičemž je specifikován příznak BCRYPT_BLOCK_PADDING.
  • Popisovač klíče byl inicializován voláním BCryptSetProperty s BCRYPT_CHAINING_MODE nastaven na BCRYPT_CHAIN_MODE_CBC.
    • Vzhledem k tomu, že BCRYPT_CHAIN_MODE_CBC je výchozí, mohl ovlivněný kód nepřiřadit žádnou hodnotu pro BCRYPT_CHAINING_MODE.

Pro programy vytvořené pomocí staršího kryptografického rozhraní API systému Windows:

  • Volání na dešifrování je na CryptDecrypt pomocí Final=TRUE.
  • Popisovač klíče byl inicializován voláním CryptSetKeyParam s KP_MODE nastaven na CRYPT_MODE_CBC.
    • Vzhledem k tomu, že CRYPT_MODE_CBC je výchozí, mohl ovlivněný kód nepřiřadit žádnou hodnotu pro KP_MODE.

Vyhledání zranitelného kódu – spravované aplikace

Vyhledání zranitelného kódu – syntaxe kryptografických zpráv

Neověřená zpráva CMS EnvelopedData, jejíž šifrovaný obsah používá režim CBC AES (2.16.840.1.101.3.4.1.2, 2.16.840.1.101.3.4.1.22, 2.16.840.1.101.3.4.1.42), DES (1.3.14.3.2.7), 3DES (1.2.840.113549.3.7) nebo RC2 (1.2.840.113549.3.2) je zranitelná, stejně jako zprávy používající všechny ostatní blokové šifrovací algoritmy v režimu CBC.

I když šifry datových proudů nejsou náchylné k tomuto konkrétnímu ohrožení zabezpečení, Microsoft doporučuje vždy ověřovat data raději než kontrolovat hodnotu ContentEncryptionAlgorithm.

U spravovaných aplikací lze objekt blob CmS EnvelopedData rozpoznat jako libovolnou hodnotu, která je předána System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

V případě nativních aplikací lze blob CMS EnvelopedData detekovat jako jakoukoli hodnotu poskytnutou popisovači CMS prostřednictvím CryptMsgUpdate, jejíž výsledný CMSG_TYPE_PARAM je CMSG_ENVELOPED a/nebo je později popisovači CMS odeslána instrukce prostřednictvím CMSG_CTRL_DECRYPT.

Příklad zranitelného kódu – spravovaný

Tato metoda čte soubor cookie a dešifruje ho a není viditelná žádná kontrola integrity dat. Obsah souboru cookie, který je přečtena touto metodou, proto může být napaden uživatelem, který ho přijal, nebo útočníkem, který získal zašifrovanou hodnotu souboru 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();
    }
}

Následující ukázkový kód používá nestandardní formát zprávy

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

Identifikátory algoritmů cipher_algorithm_id a hmac_algorithm_id jsou místní (nestandardní) reprezentace těchto algoritmů pro aplikaci. Tyto identifikátory můžou mít smysl v jiných částech vašeho existujícího protokolu zasílání zpráv místo jako holý zřetězený bajtový stream.

Tento příklad také používá jeden hlavní klíč k odvození šifrovacího klíče i klíče HMAC. To je k dispozici jako usnadnění pro přeměnu jednoklíčové aplikace na dvouklíčovou aplikaci a také k podpoře udržení dvou klíčů jako různých hodnot. Dále zaručuje, že se klíč HMAC a šifrovací klíč nedají ze synchronizace dostat.

Tento vzorek nepřijímá Stream ani pro šifrování, ani pro dešifrování. Aktuální formát dat znesnadňuje šifrování jedním průchodem, protože hodnota hmac_tag předchází šifrovanému textu. Tento formát byl však zvolen, protože uchovává všechny prvky s pevnou velikostí na začátku, aby byl analyzátor jednodušší. S tímto formátem dat je možné jednokrokové dešifrování, ačkoli se implementátorovi doporučuje zavolat GetHashAndReset a ověřit výsledek před voláním TransformFinalBlock. Pokud je šifrování streamování důležité, může se vyžadovat jiný režim AE.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                byte[] generatedTag = tagGenerator.GetHashAndReset();

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

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

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

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

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

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

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

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

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

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

                symmetricAlgorithm.Key = cipherKey;
            }

            return symmetricAlgorithm;
        }

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

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

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

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

            hmac.Key = newKey;
            return hmac;
        }

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

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

            int result = 0;

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

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

            return result == 0;
        }
    }
}

Viz také