Partilhar via


Vulnerabilidades de temporização com desencriptação simétrica no modo CBC usando preenchimento

A Microsoft acredita que não é mais seguro descriptografar dados criptografados com o modo de criptografia simétrica Cipher-Block-Chaining (CBC) quando o preenchimento verificável foi aplicado sem primeiro garantir a integridade do texto cifrado, exceto em circunstâncias muito específicas. Esta apreciação baseia-se na investigação criptográfica atualmente conhecida.

Introdução

Um ataque oráculo de preenchimento é um tipo de ataque contra dados criptografados que permite ao invasor descriptografar o conteúdo dos dados, sem saber a chave.

Um oráculo refere-se a um "dizer" que dá a um invasor informações sobre se a ação que ele está executando está correta ou não. Imagine jogar um jogo de tabuleiro ou de cartas com uma criança. Quando o rosto deles se ilumina com um grande sorriso porque eles acham que estão prestes a fazer um bom movimento, isso é um oráculo. Você, como adversário, pode usar este oráculo para planejar seu próximo movimento adequadamente.

Preenchimento é um termo criptográfico específico. Algumas cifras, que são os algoritmos usados para criptografar seus dados, funcionam em blocos de dados onde cada bloco tem um tamanho fixo. Se os dados que você deseja criptografar não tiverem o tamanho certo para preencher os blocos, seus dados serão acolchoados até que isso aconteça. Muitas formas de preenchimento exigem que o preenchimento esteja sempre presente, mesmo que a entrada original tenha o tamanho certo. Isto permite que o preenchimento seja sempre removido com segurança após a desencriptação.

Juntando as duas coisas, uma implementação de software com um oráculo de preenchimento revela se os dados descriptografados têm preenchimento válido. O oráculo pode ser algo tão simples quanto retornar um valor que diz "Preenchimento inválido" ou algo mais complicado, como levar um tempo mensurável diferente para processar um bloco válido em vez de um bloco inválido.

As cifras baseadas em bloco têm outra propriedade, chamada modo, que determina a relação dos dados no primeiro bloco com os dados no segundo bloco, e assim por diante. Um dos modos mais utilizados é o CBC. O CBC introduz um bloco aleatório inicial, conhecido como Vetor de Inicialização (IV), e combina o bloco anterior com o resultado da criptografia estática para torná-lo de tal forma que criptografar a mesma mensagem com a mesma chave nem sempre produza a mesma saída criptografada.

Um invasor pode usar um oráculo de preenchimento, em combinação com a forma como os dados CBC são estruturados, para enviar mensagens ligeiramente alteradas para o código que expõe o oráculo e continuar enviando dados até que o oráculo diga que os dados estão corretos. A partir dessa resposta, o invasor pode descriptografar a mensagem byte por byte.

As redes de computadores modernas são de tal qualidade que um invasor pode detetar diferenças muito pequenas (menos de 0,1 ms) no tempo de execução em sistemas remotos. Os aplicativos que estão assumindo que uma descriptografia bem-sucedida só pode acontecer quando os dados não foram adulterados podem ser vulneráveis a ataques de ferramentas projetadas para observar diferenças na descriptografia bem-sucedida e malsucedida. Embora essa diferença de tempo possa ser mais significativa em alguns idiomas ou bibliotecas do que em outros, agora acredita-se que essa é uma ameaça prática para todos os idiomas e bibliotecas quando a resposta do aplicativo à falha é levada em consideração.

Esse ataque depende da capacidade de alterar os dados criptografados e testar o resultado com o oráculo. A única maneira de mitigar totalmente o ataque é detetar alterações nos dados criptografados e recusar-se a executar quaisquer ações neles. A maneira padrão de fazer isso é criar uma assinatura para os dados e validar essa assinatura antes que qualquer operação seja executada. A assinatura deve ser verificável, não pode ser criada pelo invasor, caso contrário, eles alterariam os dados criptografados e, em seguida, calculariam uma nova assinatura com base nos dados alterados. Um tipo comum de assinatura apropriada é conhecido como um código de autenticação de mensagem de hash com chave (HMAC). Um HMAC difere de uma soma de verificação na medida em que utiliza uma chave secreta, conhecida apenas pela pessoa que produz o HMAC e pela pessoa que o valida. Sem a posse da chave, você não pode produzir um HMAC correto. Quando você recebe seus dados, você pega os dados criptografados, calcula independentemente o HMAC usando a chave secreta que você e o remetente compartilham e, em seguida, compara o HMAC que eles enviaram com o que você calculou. Essa comparação deve ser de tempo constante, caso contrário, você adicionou outro oráculo detetável, permitindo um tipo diferente de ataque.

Em resumo, para usar cifras de bloco CBC acolchoadas com segurança, você deve combiná-las com um HMAC (ou outra verificação de integridade de dados) que você valida usando uma comparação de tempo constante antes de tentar descriptografar os dados. Como todas as mensagens alteradas levam o mesmo tempo para produzir uma resposta, o ataque é evitado.

Quem é vulnerável

Esta vulnerabilidade aplica-se a aplicações geridas e nativas que estão a executar a sua própria encriptação e desencriptação. Isto inclui, por exemplo:

  • Uma aplicação que encripta um cookie para posterior desencriptação no servidor.
  • Um aplicativo de banco de dados que fornece aos usuários a capacidade de inserir dados em uma tabela cujas colunas são descriptografadas posteriormente.
  • Um aplicativo de transferência de dados que depende de criptografia usando uma chave compartilhada para proteger os dados em trânsito.
  • Um aplicativo que criptografa e descriptografa mensagens "dentro" do túnel TLS.

Observe que usar o TLS sozinho pode não protegê-lo nesses cenários.

Uma aplicação vulnerável:

  • Descriptografa dados usando o modo de codificação CBC com um modo de preenchimento verificável, como PKCS#7 ou ANSI X.923.
  • Executa a desencriptação sem ter realizado uma verificação da integridade dos dados (através de um MAC ou uma assinatura digital assimétrica).

Isso também se aplica a aplicativos criados sobre abstrações sobre essas primitivas, como a estrutura EnvelopedData da Sintaxe de Mensagem Criptográfica (PKCS#7/CMS).

A pesquisa levou a Microsoft a se preocupar ainda mais com as mensagens CBC que são acolchoadas com preenchimento equivalente à ISO 10126 quando a mensagem tem uma estrutura de rodapé bem conhecida ou previsível. Por exemplo, conteúdo preparado sob as regras da Sintaxe de Criptografia XML do W3C e Recomendação de Processamento (xmlenc, EncryptedXml). Embora a orientação do W3C para assinar a mensagem e depois criptografar fosse considerada apropriada na época, a Microsoft agora recomenda sempre fazer criptografar e assinar.

Os desenvolvedores de aplicativos devem estar sempre atentos à verificação da aplicabilidade de uma chave de assinatura assimétrica, pois não há nenhuma relação de confiança inerente entre uma chave assimétrica e uma mensagem arbitrária.

Detalhes

Historicamente, há consenso de que é importante criptografar e autenticar dados importantes, usando meios como assinaturas HMAC ou RSA. No entanto, tem havido orientações menos claras sobre como sequenciar as operações de encriptação e autenticação. Devido à vulnerabilidade detalhada neste artigo, a orientação da Microsoft agora é sempre usar o paradigma "criptografar e assinar". Ou seja, primeiro criptografe os dados usando uma chave simétrica e, em seguida, calcule uma assinatura MAC ou assimétrica sobre o texto cifrado (dados criptografados). Ao desencriptar dados, execute o inverso. Primeiro, confirme o MAC ou a assinatura do texto cifrado e, em seguida, desencripta-o.

Sabe-se que existe há mais de 10 anos uma classe de vulnerabilidades conhecida como "ataques oráculo de acolchoamento". Essas vulnerabilidades permitem que um invasor descriptografe dados criptografados por algoritmos de bloco simétricos, como AES e 3DES, usando não mais de 4096 tentativas por bloco de dados. Essas vulnerabilidades fazem uso do fato de que as cifras de bloco são usadas com mais frequência com dados de preenchimento verificáveis no final. Descobriu-se que, se um invasor pode adulterar texto cifrado e descobrir se a adulteração causou um erro no formato do preenchimento no final, o invasor pode descriptografar os dados.

Inicialmente, os ataques práticos eram baseados em serviços que retornavam códigos de erro diferentes com base na validade do preenchimento, como a vulnerabilidade de ASP.NET MS10-070. No entanto, a Microsoft agora acredita que é prático realizar ataques semelhantes usando apenas as diferenças de tempo entre o processamento de preenchimento válido e inválido.

Desde que o esquema de criptografia empregue uma assinatura e que a verificação de assinatura seja realizada com um tempo de execução fixo para um determinado comprimento de dados (independentemente do conteúdo), a integridade dos dados pode ser verificada sem emitir nenhuma informação para um invasor através de um canal lateral. Como a verificação de integridade rejeita quaisquer mensagens adulteradas, a ameaça do oráculo de preenchimento é atenuada.

Orientação

Em primeiro lugar, a Microsoft recomenda que todos os dados que tenham confidencialidade precisem ser transmitidos pelo Transport Layer Security (TLS), o sucessor do Secure Sockets Layer (SSL).

Em seguida, analise a sua candidatura para:

  • Entenda exatamente qual criptografia você está executando e qual criptografia está sendo fornecida pelas plataformas e APIs que você está usando.
  • Certifique-se de que cada uso em cada camada de um algoritmo de cifra de bloco simétrico, como AES e 3DES, no modo CBC incorpore o uso de uma verificação de integridade de dados com chave secreta (uma assinatura assimétrica, um HMAC ou para alterar o modo de codificação para um modo de criptografia autenticada (AE), como GCM ou CCM).

Com base na pesquisa atual, acredita-se geralmente que, quando as etapas de autenticação e criptografia são executadas independentemente para modos de criptografia não-AE, autenticar o texto cifrado (criptografar e assinar) é a melhor opção geral. No entanto, não há uma resposta única e correta para a criptografia e essa generalização não é tão boa quanto o conselho dirigido de um criptógrafo profissional.

Os aplicativos que não conseguem alterar seu formato de mensagens, mas executam a descriptografia CBC não autenticada, são encorajados a tentar incorporar atenuações como:

  • Desencriptar sem permitir que o desencriptador verifique ou remova o preenchimento:
    • Qualquer preenchimento que foi aplicado ainda precisa ser removido ou ignorado, você está movendo a carga para o seu aplicativo.
    • O benefício é que a verificação e remoção de preenchimento podem ser incorporadas a outra lógica de verificação de dados do aplicativo. Se a verificação de preenchimento e verificação de dados puder ser feita em tempo constante, a ameaça é reduzida.
    • Uma vez que a interpretação do preenchimento altera o comprimento percebido da mensagem, ainda pode haver informações de tempo emitidas a partir dessa abordagem.
  • Altere o modo de preenchimento de desencriptação para ISO10126:
    • ISO10126 preenchimento de desencriptação é compatível com o preenchimento de encriptação PKCS7 e com ANSIX923 preenchimento de encriptação.
    • A alteração do modo reduz o preenchimento do conhecimento do oráculo para 1 byte em vez do bloco inteiro. No entanto, se o conteúdo tiver um rodapé conhecido, como um elemento XML de fechamento, os ataques relacionados podem continuar a atacar o resto da mensagem.
    • Isso também não impede a recuperação de texto sem formatação em situações em que o invasor pode coagir o mesmo texto sem formatação a ser criptografado várias vezes com um deslocamento de mensagem diferente.
  • Cancele a avaliação de uma chamada de desencriptação para amortecer o sinal de temporização:
    • O cálculo do tempo de espera deve ter um mínimo superior ao tempo máximo que a operação de desencriptação levaria para qualquer segmento de dados que contenha preenchimento.
    • Os cálculos de tempo devem ser feitos de acordo com a orientação em Aquisição de carimbos de data/hora de alta resolução, não usando Environment.TickCount (sujeito a roll-over/overflow) ou subtraindo dois carimbos de data/hora do sistema (sujeitos a erros de ajuste NTP).
    • Os cálculos de tempo devem incluir a operação de desencriptação, incluindo todas as exceções potenciais em aplicações geridas ou C++, e não apenas acolchoadas no final.
    • Se o sucesso ou a falha ainda tiverem sido determinados, a porta de temporização precisará retornar a falha quando expirar.
  • Os serviços que estão executando a descriptografia não autenticada devem ter monitoramento para detetar que uma enxurrada de mensagens "inválidas" chegou.
    • Tenha em mente que este sinal carrega falsos positivos (dados legitimamente corrompidos) e falsos negativos (espalhando o ataque por um tempo suficientemente longo para escapar da deteção).

Localizando código vulnerável - aplicativos nativos

Para programas criados com base na biblioteca Windows Cryptography: Next Generation (CNG):

  • A chamada de desencriptação é para BCryptDecrypt, especificando o BCRYPT_BLOCK_PADDING sinalizador.
  • O identificador de chave foi inicializado chamando BCryptSetProperty com BCRYPT_CHAINING_MODE definido como BCRYPT_CHAIN_MODE_CBC.
    • Como BCRYPT_CHAIN_MODE_CBC é o padrão, o código afetado pode não ter atribuído nenhum valor para BCRYPT_CHAINING_MODE.

Para programas criados com base na API criptográfica mais antiga do Windows:

  • A chamada de desencriptação é para CryptDecrypt com Final=TRUE.
  • O identificador de chave foi inicializado chamando CryptSetKeyParam com KP_MODE definido como CRYPT_MODE_CBC.
    • Como CRYPT_MODE_CBC é o padrão, o código afetado pode não ter atribuído nenhum valor para KP_MODE.

Localizando código vulnerável - aplicativos gerenciados

Localizando código vulnerável - sintaxe de mensagem criptográfica

Uma mensagem CMS EnvelopedData não autenticada cujo conteúdo criptografado usa o modo CBC de 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) ou RC2 (1.2.840.113549.3.2) é vulnerável, bem como mensagens usando quaisquer outros algoritmos de cifra de bloco no modo CBC.

Embora as cifras de fluxo não sejam suscetíveis a essa vulnerabilidade específica, a Microsoft recomenda sempre autenticar os dados em vez de inspecionar o valor ContentEncryptionAlgorithm.

Para aplicativos gerenciados, um blob CMS EnvelopedData pode ser detetado como qualquer valor que é passado para System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

Para aplicativos nativos, um blob CMS EnvelopedData pode ser detetado como qualquer valor fornecido a um identificador CMS via CryptMsgUpdate cujo CMSG_TYPE_PARAM resultante é CMSG_ENVELOPED e/ou o identificador CMS é enviado posteriormente uma CMSG_CTRL_DECRYPT instrução via CryptMsgControl.

Exemplo de código vulnerável - gerenciado

Este método lê um cookie e desencripta-o e nenhuma verificação da integridade dos dados é visível. Portanto, o conteúdo de um cookie que é lido por este método pode ser atacado pelo usuário que o recebeu, ou por qualquer invasor que tenha obtido o valor do cookie criptografado.

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

O código de exemplo a seguir usa um formato de mensagem não padrão de

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

onde os cipher_algorithm_id identificadores e hmac_algorithm_id algoritmos são representações locais de aplicativos (não padrão) desses algoritmos. Esses identificadores podem fazer sentido em outras partes do seu protocolo de mensagens existente em vez de como um fluxo de bytes concatenado.

Este exemplo também usa uma única chave mestra para derivar uma chave de criptografia e uma chave HMAC. Isso é fornecido tanto como uma conveniência para transformar um aplicativo com chave única em um aplicativo de chave dupla, quanto para incentivar a manutenção das duas chaves como valores diferentes. Garante ainda que a chave HMAC e a chave de encriptação não podem ficar fora de sincronização.

Este exemplo não aceita um Stream para criptografia ou descriptografia. O formato de dados atual dificulta a criptografia em uma passagem porque o hmac_tag valor precede o texto cifrado. No entanto, esse formato foi escolhido porque mantém todos os elementos de tamanho fixo no início para manter o analisador mais simples. Com esse formato de dados, a descriptografia em uma etapa é possível, embora um implementador seja advertido para chamar GetHashAndReset e verificar o resultado antes de chamar TransformFinalBlock. Se a criptografia de streaming for importante, um modo AE diferente pode ser necessário.

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

Consulte também