Vulnérabilités de temporisation avec le déchiffrement symétrique en mode CBC à l’aide du remplissage

Microsoft estime qu’il n’est plus sûr de déchiffrer les données chiffrées avec le mode de chiffrement symétrique CBC (Cipher-Block-Chaining) lorsque le remplissage vérifiable a été appliqué sans garantir au préalable l’intégrité du texte chiffré, sauf dans des circonstances très spécifiques. Ce jugement est basé sur les recherches sur le chiffrement actuellement connues.

Introduction

Une attaque d’oracle de remplissage est un type d’attaque contre des données chiffrées qui permet à l’attaquant de déchiffrer le contenu des données, sans connaître la clé.

Un oracle fait référence à un « tell » qui donne à un attaquant des informations indiquant si l’action qu’il exécute est correcte ou non. Imaginez jouer à un jeu de société ou de cartes avec un enfant. Quand leur visage s’illumine avec un grand sourire parce qu’ils pensent être sur le point de faire un bon mouvement, c’est un oracle. Vous, en tant qu’adversaire, pouvez utiliser cet oracle pour planifier votre prochain mouvement de manière appropriée.

Le remplissage est un terme propre au chiffrement. Certains chiffrements, qui sont les algorithmes utilisés pour chiffrer vos données, fonctionnent sur des blocs de données où chaque bloc a une taille fixe. Si les données que vous souhaitez chiffrer ne sont pas de la bonne taille pour remplir les blocs, vos données sont remplies jusqu’à ce que cela soit le cas. De nombreuses formes de remplissage nécessitent que ce remplissage soit toujours présent, même si l’entrée d’origine était de la bonne taille. Cela permet au remplissage d’être toujours supprimé en toute sécurité lors du déchiffrement.

En regroupant les deux éléments, une implémentation logicielle avec un oracle de remplissage révèle si les données déchiffrées ont un remplissage valide. L’oracle peut être quelque chose d’aussi simple que de retourner une valeur indiquant « Remplissage non valide » ou quelque chose de plus compliqué, comme prendre un temps sensiblement différent pour traiter un bloc valide par rapport à un bloc non valide.

Les chiffrements basés sur des blocs ont une autre propriété, appelée mode, qui détermine la relation entre les données du premier bloc et les données du deuxième bloc, et ainsi de suite. L’un des modes les plus couramment utilisés est CBC. CBC introduit un bloc aléatoire initial, appelé vecteur d’initialisation (IV), et combine le bloc précédent avec le résultat du chiffrement statique pour que le chiffrement du même message avec la même clé ne produise pas toujours la même sortie chiffrée.

Un attaquant peut utiliser un oracle de remplissage, en combinaison avec la façon dont les données CBC sont structurées, pour envoyer des messages légèrement modifiés au code qui expose l’oracle, et continuer à envoyer des données jusqu’à ce que l’oracle lui indique que les données sont correctes. À partir de cette réponse, l’attaquant peut déchiffrer le message octet par octet.

Les réseaux informatiques modernes sont d’une qualité telle qu’un attaquant peut détecter de très petites différences (moins de 0,1 ms) dans le temps d’exécution sur les systèmes distants. Les applications qui supposent qu’un déchiffrement réussi ne peut se produire que lorsque les données n’ont pas été falsifiées peuvent être vulnérables aux attaques provenant d’outils conçus pour observer les différences entre déchiffrement réussi et infructueux. Bien que cette différence de temps puisse être plus importante dans certains langages ou bibliothèques que dans d’autres, il est maintenant considéré qu’il s’agit d’une menace concrète pour l’ensemble des langages et bibliothèques lorsque la réponse de l’application à l’échec est prise en compte.

Cette attaque s’appuie sur la possibilité de modifier les données chiffrées et de tester le résultat avec l’oracle. La seule façon d’atténuer entièrement l’attaque consiste à détecter les modifications apportées aux données chiffrées et à refuser d’effectuer des actions sur ces données. La méthode standard consiste à créer une signature pour les données et à valider cette signature avant d’effectuer des opérations. La signature doit être vérifiable ; elle ne peut pas être créée par l’attaquant, sinon il modifierait les données chiffrées, puis calculerait une nouvelle signature en fonction des données modifiées. Un type courant de signature appropriée est appelé code d’authentification de message avec hachage par clé (HMAC). Un HMAC diffère d’une somme de contrôle en cela qu’il prend une clé secrète, connue uniquement de la personne qui produit le HMAC et de la personne qui la valide. Sans la clé, vous ne pouvez pas produire un HMAC correct. Lorsque vous recevez vos données, vous prenez les données chiffrées, calculez indépendamment le HMAC à l’aide de la clé secrète que vous et l’expéditeur partagez, puis comparez le HMAC qu’il a envoyé avec celui que vous avez calculé. Cette comparaison doit être à temps constant, sinon vous avez ajouté un autre oracle détectable, ce qui permet un autre type d’attaque.

En résumé, pour utiliser des chiffrements de blocs CBC remplis en toute sécurité, vous devez les combiner avec un HMAC (ou un autre contrôle d’intégrité des données) que vous validez à l’aide d’une comparaison de temps constant avant d’essayer de déchiffrer les données. Étant donné que tous les messages modifiés prennent le même temps pour produire une réponse, l’attaque est empêchée.

Qui est vulnérable

Cette vulnérabilité s’applique aux applications managées et natives qui effectuent leur propre chiffrement et déchiffrement. Cela inclut, par exemple :

  • Une application qui chiffre un cookie pour un déchiffrement ultérieur sur le serveur.
  • Une application de base de données qui permet aux utilisateurs d’insérer des données dans une table dont les colonnes sont ensuite déchiffrées.
  • Une application de transfert de données qui s’appuie sur le chiffrement à l’aide d’une clé partagée pour protéger les données en transit.
  • Une application qui chiffre et déchiffre les messages « à l’intérieur » du tunnel TLS.

Notez que l’utilisation de TLS seul peut ne pas vous protéger dans ces scénarios.

Une application vulnérable :

  • Déchiffre les données à l’aide du mode de chiffrement CBC avec un mode de remplissage vérifiable, comme PKCS#7 ou ANSI X.923.
  • Effectue le déchiffrement sans avoir effectué de vérification d’intégrité des données (via un MAC ou une signature numérique asymétrique).

Cela s’applique également aux applications basées sur des abstractions sur ces primitives, comme la structure EnvelopedData de la syntaxe de message de chiffrement (PKCS#7/CMS).

La recherche a conduit Microsoft à se préoccuper davantage des messages CBC qui sont remplis avec un remplissage équivalent à ISO 10126 lorsque le message a une structure de pied de page connue ou prévisible. Par exemple, le contenu préparé selon les règles recommandées de syntaxe et de traitement du chiffrement XML W3C (xmlenc, EncryptedXml). Alors que les instructions du W3C pour signer puis chiffrer le message étaient considérées comme appropriées à l’époque, Microsoft recommande désormais de toujours effectuer le chiffrement, puis la signature.

Les développeurs d’applications doivent toujours veiller à vérifier l’applicabilité d’une clé de signature asymétrique, car il n’existe aucune relation d’approbation inhérente entre une clé asymétrique et un message arbitraire.

Détails

Historiquement, il y a eu consensus sur le fait qu’il est important de chiffrer et d’authentifier les données importantes, à l’aide de moyens comme les signatures HMAC ou RSA. Toutefois, les instructions étaient moins claires sur la façon de séquencer les opérations de chiffrement et d’authentification. En raison de la vulnérabilité décrite dans cet article, Microsoft vous conseille de toujours suivre le paradigme « chiffrer, puis signer ». Autrement dit, chiffrez d’abord les données à l’aide d’une clé symétrique, puis calculez une signature MAC ou asymétrique sur le texte chiffré (données chiffrées). Lors du déchiffrement des données, effectuez l’opération inverse. Confirmez d’abord le MAC ou la signature du texte chiffré, puis déchiffrez-le.

Une classe de vulnérabilités appelée « attaques d’oracle de remplissage » est connue depuis plus de 10 ans. Ces vulnérabilités permettent à un attaquant de déchiffrer des données chiffrées par des algorithmes de blocs symétriques, comme AES et 3DES, en utilisant au maximum 4 096 tentatives par bloc de données. Ces vulnérabilités utilisent le fait que les chiffrements par blocs sont le plus fréquemment utilisés avec des données de remplissage vérifiables à la fin. Il a été constaté que, si un attaquant peut falsifier le texte chiffré et déterminer si la falsification a provoqué une erreur dans le format du remplissage à la fin, l’attaquant peut déchiffrer les données.

Initialement, les attaques concrètes étaient basées sur des services qui retournaient différents codes d’erreur selon que le remplissage était valide ou non, comme la vulnérabilité ASP.NET MS10-070. Toutefois, Microsoft estime maintenant qu’il est possible de mener des attaques similaires en utilisant uniquement les différences de délai entre le traitement d’un remplissage valide et non valide.

À condition que le schéma de chiffrement utilise une signature et que la vérification de la signature soit effectuée avec un runtime fixe pour une longueur de données particulière (quel que soit le contenu), l’intégrité des données peut être vérifiée sans émettre d’informations à un attaquant via un canal latéral. Étant donné que la vérification de l’intégrité rejette les messages falsifiés, la menace d’oracle de remplissage est atténuée.

Guidance

Tout d’abord, Microsoft recommande que toutes les données qui ont besoin de confidentialité soient transmises via TLS (Transport Layer Security), le successeur du protocole SSL (Secure Sockets Layer).

Ensuite, analysez votre application pour :

  • Comprendre précisément le chiffrement que vous effectuez et le chiffrement fourni par les plateformes et les API que vous utilisez.
  • Vous assurer que chaque utilisation à chaque couche d’un algorithme de chiffrement de bloc symétrique, comme AES et 3DES, en mode CBC, incorpore l’utilisation d’un contrôle d’intégrité des données à clé secrète (une signature asymétrique, un HMAC ou, pour modifier le mode de chiffrement en chiffrement authentifié (AE), un mode tel que GCM ou CCM).

D’après la recherche actuelle, il est généralement admis que lorsque les étapes d’authentification et de chiffrement sont effectuées indépendamment pour les modes de chiffrement non AE, l’authentification du texte chiffré (chiffrer puis signer) est la meilleure option générale. Toutefois, il n’y a pas de réponse universelle pour le chiffrement, et cette généralisation n’est pas aussi bonne que les conseils d’un cryptographe professionnel.

Les applications qui ne peuvent pas modifier leur format de messagerie mais effectuent un déchiffrement CBC non authentifié sont encouragées à essayer d’incorporer des atténuations comme les suivantes :

  • Déchiffrez sans autoriser le déchiffreur à vérifier ou supprimer le remplissage :
    • Tout remplissage appliqué doit toujours être supprimé ou ignoré. Vous déplacez la charge dans votre application.
    • L’avantage est que la vérification et la suppression du remplissage peuvent être incorporées dans d’autres logiques de vérification des données d’application. Si la vérification du remplissage et la vérification des données peuvent être effectuées en temps constant, la menace est réduite.
    • Étant donné que l’interprétation du remplissage change la longueur perçue du message, des informations de minutage peuvent encore être émises par cette approche.
  • Remplacez le mode de remplissage de déchiffrement par ISO10126 :
    • Le remplissage de déchiffrement ISO10126 est compatible avec le remplissage de chiffrement PKCS7 et le remplissage de chiffrement ANSIX923.
    • La modification du mode réduit les connaissances de l’oracle de remplissage à 1 octet au lieu du bloc entier. Toutefois, si le contenu a un pied de page bien connu, comme un élément XML fermant, les attaques associées peuvent continuer à attaquer le reste du message.
    • Cela n’empêche pas non plus la récupération de texte en clair dans les situations où l’attaquant peut contraindre le même texte en clair à être chiffré plusieurs fois avec un décalage de message différent.
  • Contrôlez l’évaluation d’un appel de déchiffrement pour atténuer le signal de minutage :
    • Le calcul de la durée de conservation doit avoir un minimum supérieur à la durée maximale nécessaire à l’opération de déchiffrement pour tout segment de données qui contient un remplissage.
    • Les calculs de temps doivent être effectués conformément aux instructions fournies dans Acquisition de timestamps à haute résolution, et non en utilisant Environment.TickCount (sous réserve de substitution/dépassement de capacité) ou en soustrayant deux timestamps système (sous réserve d’erreurs d’ajustement NTP).
    • Les calculs de temps doivent inclure l’opération de déchiffrement, y compris toutes les exceptions potentielles dans les applications managées ou C++, et non pas simplement celles remplies à la fin.
    • Si la réussite ou l’échec a été déterminé, la vérification de minutage doit retourner l’échec à son expiration.
  • Les services qui effectuent un déchiffrement non authentifié doivent avoir une surveillance en place pour détecter qu’un flot de messages « non valides » s’est produit.
    • Gardez à l’esprit que ce signal comporte à la fois des faux positifs (données légitimement endommagées) et des faux négatifs (étalage de l’attaque sur une période suffisamment longue pour échapper à la détection).

Recherche de code vulnérable - applications natives

Pour les programmes créés sur la bibliothèque Cryptography : Next Generation (CNG) Windows :

  • L’appel de déchiffrement est pour BCryptDecrypt, en spécifiant l’indicateur BCRYPT_BLOCK_PADDING.
  • Le descripteur de clé a été initialisé en appelant BCryptSetProperty, avec BCRYPT_CHAINING_MODE défini sur BCRYPT_CHAIN_MODE_CBC.
    • Étant donné que BCRYPT_CHAIN_MODE_CBC est la valeur par défaut, le code affecté n’a peut-être pas affecté de valeur pour BCRYPT_CHAINING_MODE.

Pour les programmes créés sur l’ancienne API de chiffrement Windows :

  • L’appel de déchiffrement est sur CryptDecrypt, avec Final=TRUE.
  • Le descripteur de clé a été initialisé en appelant CryptSetKeyParam, avec KP_MODE défini sur CRYPT_MODE_CBC.
    • Étant donné que CRYPT_MODE_CBC est la valeur par défaut, le code affecté n’a peut-être pas affecté de valeur pour KP_MODE.

Recherche de code vulnérable - Applications managées

Recherche de code vulnérable - syntaxe de message de chiffrement

Un message CMS EnvelopedData non authentifié dont le contenu chiffré utilise le mode CBC d’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) est vulnérable, ainsi que les messages utilisant tout autre algorithme de chiffrement de bloc en mode CBC.

Bien que les chiffrements de flux ne soient pas sensibles à cette vulnérabilité particulière, Microsoft recommande de toujours authentifier les données plutôt que d’inspecter la valeur ContentEncryptionAlgorithm.

Pour les applications managées, un objet blob CMS EnvelopedData peut être détecté comme toute valeur passée à System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

Pour les applications natives, un objet blob CMS EnvelopedData peut être détecté en tant que valeur fournie à un descripteur CMS via CryptMsgUpdate, dont le CMSG_TYPE_PARAM résultant est CMSG_ENVELOPED et/ou le descripteur CMS reçoit ultérieurement une instruction CMSG_CTRL_DECRYPT via CryptMsgControl.

Exemple de code vulnérable - managé

Cette méthode lit un cookie et le déchiffre. Aucune vérification d’intégrité des données n’est visible. Par conséquent, le contenu d’un cookie lu par cette méthode peut être attaqué par l’utilisateur qui l’a reçu, ou par tout attaquant qui a obtenu la valeur de cookie chiffrée.

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

L’exemple de code suivant utilise un format de message non standard

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

où les identificateurs d’algorithme cipher_algorithm_id et hmac_algorithm_id sont des représentations locales d’application (non standard) de ces algorithmes. Ces identificateurs peuvent avoir un sens dans d’autres parties de votre protocole de messagerie existant plutôt qu’en tant qu’octet concaténé nu.

Cet exemple utilise également une clé principale unique pour dériver à la fois une clé de chiffrement et une clé HMAC. Cela est fourni à la fois par commodité pour transformer une application à clé unique en une application à double clé, et pour vous encourager à conserver les deux clés comme valeurs différentes. Cela garantit en outre que la clé HMAC et la clé de chiffrement ne peuvent pas être désynchronisées.

Cet exemple n’accepte pas de Stream pour le chiffrement ou le déchiffrement. Le format de données actuel rend le chiffrement en une seule passe difficile, car la valeur hmac_tag précède le texte chiffré. Toutefois, ce format a été choisi, car il conserve tous les éléments de taille fixe au début pour simplifier l’analyseur. Avec ce format de données, le déchiffrement en une seule passe est possible, bien qu’un implémenteur soit averti d’appeler GetHashAndReset et de vérifier le résultat avant d’appeler TransformFinalBlock. Si le chiffrement de diffusion en continu est important, un autre mode AE peut être nécessaire.

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

Voir aussi