Vulnerabilità di temporizzazione con decrittografia simmetrica modalità CBC usando il riempimento

Microsoft ritiene non sia più sicuro decrittografare dati crittografati con la crittografia simmetrica in modalità CBC (Cipher-Block-Chaining) quando la spaziatura interna verificabile sia stata applicata senza prima garantire l'integrità del testo crittografato, fatta eccezione per circostanze molto specifiche. Queste conclusioni si basano sulla ricerca crittografica attualmente nota.

Introduzione

Un attacco oracolo della spaziatura interna è un tipo di attacco ai dati crittografati che consente all'utente malintenzionato di decrittografare il contenuto dei dati senza conoscere la chiave.

Un oracolo fa riferimento a un "tell" che informa l’utente malintenzionato se l'azione in esecuzione sia corretta o meno. Si immagini di star giocando a un gioco da tavola o di carte con un bambino. Quando il suo volto si illumina con un largo sorriso perché crede di star per fare un’ottima mossa, questo è un oracolo. L’avversario può usare l’oracolo per pianificare la prossima mossa.

La spaziatura interna è un termine di crittografia specifico. Alcune crittografie, ovvero gli algoritmi usati per crittografare i dati, funzionano su blocchi di dati in cui ogni blocco è una dimensione fissa. Se i dati da crittografare non sono delle dimensioni corrette per riempire i blocchi, i dati vengono riempiti internamente fino a quando non i blocchi non sono pieni. Molte forme di spaziatura interna richiedono che questa sia sempre presente, anche se l'input originale è di dimensioni corrette. In questo modo, la spaziatura interna viene sempre rimossa in modo sicuro al momento della decrittografia.

Sommando questi due concetti, un'implementazione software con un oracolo di spaziatura interna rivela se i dati decrittografati hanno una spaziatura interna valida. L'oracolo può semplicemente consistere nella restituzione di un valore che indica "Riempimento non valido" o in qualcosa di più complesso, come impiegare un tempo quantificabilmente diverso per processare un blocco valido rispetto a uno invalido.

Le crittografie basate su blocchi hanno un'altra proprietà, denominata modalità, che determina la relazione dei dati nel primo blocco ai dati nel secondo blocco, e così via. Una delle modalità più comunemente usate è CBC. CBC introduce un blocco casuale iniziale, noto come Vettore di inizializzazione (IV), e combina il blocco precedente con il risultato della crittografia statica così che la crittografia dello stesso messaggio con la stessa chiave non produca sempre lo stesso output crittografato.

Un utente malintenzionato può usare un oracolo di spaziatura interna, in combinazione con la struttura dei dati CBC, per inviare messaggi leggermente modificati al codice che espone l'oracolo e continuare a inviare dati fino a quando l'oracolo non indica che questi ultimi sono corretti. Da questa risposta, l'utente malintenzionato può decrittografare il messaggio byte per byte.

Le reti informatiche moderne sono di qualità talmente alta che un utente malintenzionato può rilevare differenze molto piccole (inferiori a 0,1 ms) nel tempo di esecuzione nei sistemi remoti. Le applicazioni che presuppongono che una decrittografia corretta possa verificarsi solo quando i dati non sono stati manomessi potrebbero essere vulnerabili ad attacchi da parte di strumenti progettati per osservare le differenze tra decrittografie riuscite e non riuscite. Anche se questa differenza di intervallo può essere più significativa in alcuni linguaggi o librerie rispetto ad altre, si ritiene che questa sia una minaccia reale per tutte le lingue e le librerie quando si tiene conto della risposta dell'applicazione a un errore.

Questo attacco si basa sulla possibilità di modificare i dati crittografati e testare il risultato con l'oracolo. L'unico modo per attenuare completamente l'attacco consiste nel rilevare le modifiche ai dati crittografati e rifiutare di eseguire azioni su di essi. Il metodo standard per eseguire questa operazione consiste nel creare una firma per i dati e convalidare la firma prima dell'esecuzione di qualsiasi operazione. La firma deve essere verificabile e non può essere creata dall'utente malintenzionato, o questo modificherebbe i dati crittografati e renderebbe necessaria la creazione di una nuova firma basata sui dati modificati. Un tipo diffuso di firma adatta è noto come codice HMAC (Keyed-Hash Message Authentication Code). Un HMAC è diverso da un checksum in quanto accetta una chiave privata, nota solo a chi produce il HMAC e chi lo convalida. Se non si è in possesso della chiave, non è possibile produrre un HMAC corretto. Quando si ricevono i dati, i dati crittografati vanno calcolati indipendentemente usando la chiave privata e la condivisione del mittente, quindi l'HMAC inviato deve essere confrontato con quello calcolato. Questo confronto deve essere costante, o si aggiungerebbe un altro oracolo rilevabile, consentendo un diverso tipo di attacco.

In sintesi, per usare crittografie a blocchi CBC con spaziatura interna in modo sicuro, è necessario combinarle con un controllo HMAC (o un altro controllo di integrità dei dati) convalidato usando un confronto temporale costante prima di tentare di decrittografare i dati. Poiché tutti i messaggi modificati richiedono lo stesso tempo per produrre una risposta, l'attacco viene prevenuto.

Chi è vulnerabile

Questa vulnerabilità riguarda sia le applicazioni gestite che native che eseguono la propria crittografia e decrittografia. Questo include, ad esempio:

  • Applicazione che crittografa un cookie per poi decrittografarlo in un secondo momento nel server.
  • Applicazione di database che consente agli utenti di inserire dati in una tabella le cui colonne vengono decrittografate in un secondo momento.
  • Applicazione di trasferimento dati che si basa sulla crittografia usando una chiave condivisa per proteggere i dati in transito.
  • Applicazione che crittografa e decrittografa messaggi "all'interno" del tunnel TLS.

Si noti che usare esclusivamente TLS potrebbe non bastare a proteggere l'utente in questi scenari.

Applicazione vulnerabile:

  • Decrittografa i dati usando la modalità di crittografia CBC con una modalità di spaziatura interna verificabile, ad esempio PKCS#7 o ANSI X.923.
  • Esegue la decrittografia senza aver eseguito un controllo dell'integrità dei dati (tramite un MAC o una firma digitale asimmetrica).

Questo vale anche per applicazioni basate su astrazioni a loro volta basate su tali primitive, come la struttura EnvelopedData (Cryptographic Message Syntax, PKCS#7/CMS).

La ricerca ha portato Microsoft a preoccuparsi ulteriormente dei messaggi CBC con spaziatura interna equivalente a ISO 10126 quando il messaggio ha una struttura piè di pagina nota o prevedibile. Ad esempio, contenuti preparati in base alle regole Sintassi ed elaborazione di crittografia XML W3C (xmlenc, EncryptedXml). Anche se le linee guida W3C per la firma del messaggio crittografato erano considerate adatte, Microsoft ora consiglia di eseguire sempre la crittografia prima della firma.

Gli sviluppatori di applicazioni devono sempre tenere in considerazione la verifica dell'applicabilità di una chiave di firma asimmetrica, poiché non esiste alcuna relazione di fiducia intrinseca tra una chiave asimmetrica e un messaggio arbitrario.

Dettagli

Storicamente, è stabilito che sia fondamentale crittografare e autenticare i dati importanti, usando mezzi come le firme HMAC o RSA. Tuttavia, le indicazioni fornite su come sequenziare le operazioni di crittografia e autenticazione sono meno chiare. A causa della vulnerabilità descritta in questo articolo, le correnti linee guida di Microsoft usano sempre il paradigma "crittografia e firma". Ovvero, crittografare prima i dati usando una chiave simmetrica, quindi calcolare un MAC o una firma asimmetrica sul testo crittografato (dati crittografati). Per la decrittografia dei dati, eseguire l'operazione inversa. Prima di tutto, confermare il MAC o la firma del testo crittografato, quindi decrittografarlo.

Una classe di vulnerabilità definite "attacchi oracle di spaziatura interna" è nota da oltre 10 anni. Queste vulnerabilità consentono a un utente malintenzionato di decrittografare i dati crittografati da algoritmi a blocchi simmetrici, come AES e 3DES, usando non più di 4096 tentativi per blocco di dati. Queste vulnerabilità sfruttano il fatto che le crittografie a blocchi vengono usate principalmente con dati di spaziatura interna verificabili finali. È stato rilevato che se un utente malintenzionato può manomettere il testo crittografato e scoprire se la manomissione ha causato un errore nel formato della spaziatura interna finale, l'autore dell'attacco può decrittografare i dati.

Inizialmente, gli attacchi pratici erano basati su servizi che restituivano codici di errore diversi in base alla validità della spaziatura interna, ad esempio la vulnerabilità di ASP.NET MS10-070. Tuttavia, Microsoft ora ritiene sia pratico condurre attacchi simili usando solo le differenze di tempo tra l'elaborazione di spaziatura interna valida e quella non valida.

A condizione che lo schema di crittografia usi una firma e che la verifica della firma venga eseguita con un runtime fisso per una determinata lunghezza di dati (indipendentemente dal contenuto), l'integrità dei dati può essere verificata senza emettere informazioni a un utente malintenzionato tramite un canale laterale. Poiché il controllo di integrità rifiuta eventuali messaggi manomessi, la minaccia oracle della spaziatura interna viene attenuata.

Indicazioni

In primo luogo, Microsoft consiglia di trasmettere tutti i dati riservati tramite Transport Layer Security (TLS), il successore di Secure Sockets Layer (SSL).

Quindi, analizzare l'applicazione per:

  • Comprendere precisamente quale crittografia sia eseguita e quale sia fornita dalle piattaforme e dalle API in uso.
  • Assicurarsi che ogni utilizzo a ogni livello di un algoritmo di crittografia a blocchi simmetrico (come AES e 3DES) in modalità CBC incorpori l'uso di un controllo di integrità dei dati con chiave privata (una firma asimmetrica, un HMAC o la modifica della modalità crittografia a una modalità crittografia autenticata (AE), come GCM o CCM).

In base alla ricerca corrente, si ritiene in genere che quando i passaggi di autenticazione e crittografia vengono eseguiti indipendentemente per le modalità di crittografia non AE, l'autenticazione del testo crittografato (crittografia e firma) è l'opzione migliore. Tuttavia, non c'è una soluzione di crittografia adatta a tutti i casi, e indicazioni generali non saranno mai efficaci quanto la consulenza di un crittografo professionale.

Per applicazioni che non sono in grado di modificare il formato di messaggistica, ma che eseguono la decrittografia CBC non autenticata, è consigliabile provare a incorporare mitigazioni quali:

  • Decrittografare senza consentire al decrittografatore di verificare o rimuovere la spaziatura interna:
    • Qualsiasi spaziatura interna applicata deve comunque essere rimossa o ignorata, spostando il carico nell'applicazione.
    • Il vantaggio è che la verifica e la rimozione della spaziatura interna possono essere incorporate in altre logiche di verifica dei dati dell'applicazione. Se la verifica della spaziatura interna e la verifica dei dati possono essere eseguite in maniera costante, la minaccia viene ridotta.
    • Poiché l'interpretazione della spaziatura interna modifica la lunghezza percepita del messaggio, questo approccio potrebbe comunque generare informazioni di intervallo.
  • Modificare la modalità di spaziatura interna della decrittografia a ISO10126:
    • La spaziatura interna di decrittografia ISO10126 è compatibile con la spaziatura interna della crittografia PKCS7 e la spaziatura interna della crittografia ANSIX923.
    • Modificare la modalità riduce la conoscenza oracolo della spaziatura interna a 1 byte anziché l'intero blocco. Tuttavia, se il contenuto ha un piè di pagina noto (ad esempio, un elemento XML di chiusura), gli attacchi correlati possono continuare ad attaccare il resto del messaggio.
    • Inoltre, ciò non impedisce il ripristino di testo non crittografato in situazioni in cui l'utente malintenzionato può forzare lo stesso testo non crittografato a essere crittografato più volte con un offset di messaggio diverso.
  • Controllare la valutazione di una chiamata di decrittografia per attenuare il segnale di tempo:
    • Il calcolo del tempo di attesa deve avere un minimo superiore alla quantità massima di tempo necessario per l'operazione di decrittografia per qualsiasi segmento di dati che contiene spaziatura interna.
    • I calcoli temporali devono essere eseguiti in base alle indicazioni riportate in Acquisizione di timestamp ad alta risoluzione, non usando Environment.TickCount (soggetto a rollover/overflow) o sottraendo due timestamp di sistema (soggetti a errori di rettifica NTP).
    • I calcoli temporali devono includere l'operazione di decrittografia, comprese tutte le potenziali eccezioni nelle applicazioni gestite o C++, non solo con spaziatura interna fino alla fine.
    • Se l'esito positivo o negativo non è stato ancora determinato, il gate di intervallo deve restituire un errore alla scadenza.
  • I servizi che eseguono la decrittografia non autenticata devono eseguire il monitoraggio in modo da rilevare un'inondazione di messaggi "non validi".
    • Tenere presente che questo segnale trasporta sia falsi positivi (dati effettivamente danneggiati) che falsi negativi (che prolungano l’attacco abbastanza da eludere il rilevamento).

Ricerca di codice vulnerabile: applicazioni native

Per programmi compilati in base alla libreria CNG (Cryptography: Next Generation):

  • La chiamata di decrittografia è a BCryptDecrypt, specificando il flag BCRYPT_BLOCK_PADDING.
  • L'handle della chiave è stato inizializzato chiamando BCryptSetProperty con BCRYPT_CHAINING_MODE impostato su BCRYPT_CHAIN_MODE_CBC.
    • Poiché BCRYPT_CHAIN_MODE_CBC è l'impostazione predefinita, il codice interessato potrebbe non avere assegnato alcun valore per BCRYPT_CHAINING_MODE.

Per i programmi compilati con l'API di crittografia di Windows precedente:

  • La chiamata di decrittografia è a CryptDecrypt con Final=TRUE.
  • L'handle della chiave è stato inizializzato chiamando CryptSetKeyParam con KP_MODE impostato su CRYPT_MODE_CBC.
    • Poiché CRYPT_MODE_CBC è l'impostazione predefinita, il codice interessato potrebbe non avere assegnato alcun valore per KP_MODE.

Ricerca di codice vulnerabile: applicazioni gestite

Ricerca di codice vulnerabile: sintassi dei messaggi crittografici

Un messaggio CMS EnvelopedData non autenticato il cui contenuto crittografato usa la modalità CBC di 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) o RC2 (1.2.840.113549.3.2), nonché messaggi che usano qualsiasi altro algoritmo di crittografia a blocchi in modalità CBC, sono vulnerabili.

Anche se le crittografie di flusso non sono soggette a questa particolare vulnerabilità, Microsoft consiglia di autenticare sempre i dati durante l'ispezione del valore ContentEncryptionAlgorithm.

Per applicazioni gestite, è possibile rilevare un blob di CMS EnvelopedData come qualsiasi valore passato a System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

Per applicazioni native, un BLOB CMS EnvelopedData può essere rilevato come qualsiasi valore fornito a un handle CMS tramite CryptMsgUpdate, il cui CMSG_TYPE_PARAM risultante è CMSG_ENVELOPED e/o un’istruzione CMSG_CTRL_DECRYPT viene successivamente fornita all'handle CMS tramite CryptMsgControl.

Esempio di codice vulnerabile: gestito

Questo metodo legge un cookie e lo decrittografa senza che alcun controllo di integrità dei dati sia visibile. Di conseguenza, il contenuto di un cookie letto da questo metodo può essere attaccato dall'utente che lo ha ricevuto o da qualsiasi utente malintenzionato che sia in possesso del valore crittografato del 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();
    }
}

Il seguente codice esempio usa un formato di messaggio non standard

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

dove gli identificatori di algoritmo cipher_algorithm_id e hmac_algorithm_id sono rappresentazioni locali dell'applicazione (non standard) di tali algoritmi. Questi identificatori possono avere senso in altre parti del protocollo di messaggistica esistente anziché come puro bytestream concatenato.

In questo esempio viene usata anche una singola chiave master per derivare sia una chiave di crittografia che una chiave HMAC. Questa opzione viene fornita sia come metodo pratico per trasformare un'applicazione con chiave singola in un'applicazione con chiave doppia, sia per incoraggiare la conservazione delle due chiavi come valori diversi. Garantisce inoltre che la chiave HMAC e la chiave di crittografia non possano uscire dalla sincronizzazione.

Questo esempio non accetta un Stream per la crittografia o la decrittografia. Il formato dati corrente rende difficile la crittografia a un passaggio, poiché il valore hmac_tag precede il testo crittografato. Tuttavia, questo formato è stato scelto perché mantiene tutti gli elementi a dimensione fissa all'inizio così da semplificare il parser. Con questo formato di dati, la dectrittografia a un solo passaggio è possibile, anche se si consiglia all’implementatore di chiamare GetHashAndReset e verificare il risultato prima di chiamare TransformFinalBlock. Se la crittografia di streaming è importante, potrebbe essere necessaria una modalità AE diversa.

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

Vedi anche