Verwenden der Auffüllung für die Zeitsteuerung bei Sicherheitsrisiken mit symmetrischer Entschlüsselung im CBC-Modus

Microsoft ist der Ansicht, dass es nicht mehr sicher ist, Daten zu entschlüsseln, die mit dem CBC-Modus (Cipher-Block-Chaining) der symmetrischen Verschlüsselung unter Anwendung einer verifizierbaren Auffüllung verschlüsselt wurden, ohne zuvor die Integrität des Chiffretexts zu gewährleisten. Dabei stellen nur sehr spezifische Umstände eine Ausnahme dar. Diese Einschätzung basiert auf dem aktuellen Stand der Kryptografieforschung.

Einführung

Ein Padding-Oracle-Angriff ist ein Angriff auf verschlüsselte Daten, der es Angreifer*innen ermöglicht, den Inhalt der Daten zu entschlüsseln, ohne den Schlüssel zu kennen.

Ein Oracle liefert Angreifer*innen Informationen durch einen sogenannten „Tell“, ob die ausgeführte Aktion korrekt ist oder nicht. Stellen Sie sich vor, dass Sie ein Brett- oder Kartenspiel mit einem Kind spielen. Wenn das Kind plötzlich grinst, weil es denkt, dass es gerade einen guten Zug macht, ist das ein Oracle. Sie als Gegenüber können sich dieses Oracle zunutze machen, um Ihren nächsten Zug zu planen.

Auffüllung ist ein Begriff aus der Kryptografie. Einige Verfahren, die zum Verschlüsseln Ihrer Daten verwendet werden, arbeiten mit Datenblöcken, bei denen jeder Block eine feste Größe hat. Wenn die Daten, die Sie verschlüsseln möchten, nicht die richtige Größe haben, um die Blöcke zu füllen, werden die Daten aufgefüllt, bis das der Fall ist. Viele Formen der Auffüllung setzen voraus, dass die Auffüllung immer erfolgt, auch wenn die ursprüngliche Eingabe die richtige Größe hatte. Dadurch kann die Auffüllung bei der Entschlüsselung immer sicher entfernt werden.

Eine Softwareimplementierung mit einem Padding Oracle enthüllt also, ob entschlüsselte Daten über eine gültige Auffüllung verfügen. Das Oracle könnte einfach einen Wert zurückgeben, der „Ungültige Auffüllung“ besagt, oder ein komplizierteres Verfahren anwenden, bei dem beispielsweise der Zeitunterschied zwischen der Verarbeitung eines gültigen und eines ungültigen Blocks gemessen wird.

Blockbasierte Verschlüsselungsverfahren haben eine weitere Eigenschaft, die als Modus bezeichnet wird, die die Beziehung der Daten im ersten Block zu den Daten im zweiten Block bestimmt – und so weiter. Einer der am häufigsten verwendeten Modi ist CBC. CBC führt einen ersten zufälligen Block ein, der als Initialisierungsvektor (IV) bezeichnet wird, und kombiniert den vorherigen Block mit dem Ergebnis der statischen Verschlüsselung, sodass die Verschlüsselung derselben Nachricht mit demselben Schlüssel nicht immer die gleiche verschlüsselte Ausgabe erzeugt.

Angreifer*innen können ein Padding Oracle in Kombination mit der Struktur von CBC-Daten verwenden, um so lange leicht abgeänderte Nachrichten an den Code des Oracles zu senden, bis dieses mitteilt, dass die Daten korrekt sind. Anhand dieser Antwort können Angreifer*innen die Nachricht Byte für Byte entschlüsseln.

Moderne Computernetzwerke sind von so hoher Qualität, dass Angreifer*innen minimale Unterschiede (weniger als 0,1 ms) in der Ausführungszeit auf Remotesystemen erkennen können. Anwendungen, die davon ausgehen, dass eine erfolgreiche Entschlüsselung nur erfolgen kann, wenn die Daten nicht manipuliert wurden, sind möglicherweise anfällig für Angriffe über Tools, die dafür entwickelt wurden, die Unterschiede bei einer erfolgreichen und nicht erfolgreichen Entschlüsselung zu erkennen. Obwohl dieser zeitliche Unterschied in einigen Sprachen oder Bibliotheken wichtiger ist als in anderen, wird jetzt angenommen, dass es eine reale Bedrohung für alle Sprachen und Bibliotheken darstellt, wenn die Reaktionszeit der Anwendung auf Fehler berücksichtigt wird.

Dieser Angriff basiert auf der Möglichkeit, die verschlüsselten Daten zu ändern und das Ergebnis mit dem Oracle zu testen. Die einzige Möglichkeit, den Angriff vollständig zu verhindern, besteht darin, Änderungen an den verschlüsselten Daten zu erkennen und jegliche Interaktion mit diesen zu verweigern. Die übliche Methode besteht darin, eine Signatur für die Daten zu erstellen und diese zu verifizieren, bevor Vorgänge ausgeführt werden. Die Signatur muss verifizierbar sein und darf nicht durch Angreifer*innen erstellt werden. Andernfalls würden diese die verschlüsselten Daten ändern und dann basierend auf den geänderten Daten eine neue Signatur berechnen. Eine gängige Signatur für diese Zwecke ist der „Hash-based Message Authentication Code“ (HMAC). Ein HMAC unterscheidet sich von einer Prüfsumme darin, dass er einen geheimen Schlüssel benötigt, der nur der Person bekannt ist, die den HMAC erstellt, und der Person, die ihn verifiziert. Ohne den Besitz des Schlüssels können Sie keinen korrekten HMAC erstellen. Wenn Sie Ihre verschlüsselten Daten erhalten, würden Sie den HMAC mit dem geheimen Schlüssel, den Sie und der oder die Absender*in gemeinsam verwenden, unabhängig voneinander berechnen und dann den gesendeten HMAC mit dem von Ihnen berechneten vergleichen. Dieser Vergleich muss mit einer konstanten Zeit erfolgen. Andernfalls haben Sie ein weiteres erkennbares Oracle hinzugefügt, das eine andere Art von Angriff zulässt.

Um aufgefüllte CBC-Blockchiffren sicher zu verwenden, müssen Sie diese also mit einem HMAC (oder einer anderen Datenintegritätsprüfung) kombinieren, die Sie mithilfe eines konstanten Zeitvergleichs verifizieren, bevor Sie versuchen, die Daten zu entschlüsseln. Da alle geänderten Nachrichten die gleiche Antwortzeit aufweisen, wird der Angriff verhindert.

Anfällige Systeme

Dieses Sicherheitsrisiko betrifft sowohl verwaltete als auch native Anwendungen, die ihre eigene Verschlüsselung und Entschlüsselung durchführen. Dazu gehören beispielsweise:

  • Anwendungen, die ein Cookie zur späteren Entschlüsselung auf dem Server verschlüsseln
  • Datenbankanwendungen, die es Benutzer*innen ermöglichen, Daten in eine Tabelle einzufügen, deren Spalten später entschlüsselt werden
  • Datenübertragungsanwendungen, die auf der Verschlüsselung mit einem gemeinsam verwendeten Schlüssel basieren, um die übertragenen Daten zu schützen
  • Anwendungen, die Nachrichten innerhalb des TLS-Tunnels verschlüsseln und entschlüsseln

Beachten Sie, dass TLS in diesen Szenarios möglicherweise nicht als Schutzmaßnahme ausreicht.

Anfällige Anwendungen:

  • entschlüsseln Daten mithilfe des CBC-Verschlüsselungsmodus mit einem verifizierbaren Auffüllungsmodus, z. B. PKCS#7 oder ANSI X.923
  • führen die Entschlüsselung durch, ohne eine Datenintegritätsprüfung (über MAC oder eine asymmetrische digitale Signatur) durchgeführt zu haben

Das gilt auch für Anwendungen, die auf Abstraktionen dieser Primitive basieren, z. B. der EnvelopedData-Struktur für kryptografische Nachrichtensyntax (PKCS#7/CMS).

Microsoft hat durch Forschung weiterhin festgestellt, dass Bedenken bezüglich CBC-Nachrichten bestehen, die mit einer ISO 10126-konformen Auffüllung verarbeitet wurden, wenn die Nachricht eine bekannte oder vorhersagbare Fußzeilenstruktur aufweist. Das gilt beispielsweise für Inhalte, die nach den Regeln der XML-Verschlüsselungssyntax und Verarbeitungsempfehlungen der W3C (xmlenc, EncryptedXml) aufbereitet wurden. Die Empfehlung des W3C, Nachrichten zuerst zu signieren und dann zu verschlüsseln, war zum damaligen Zeitpunkt angemessen. Microsoft empfiehlt nun jedoch, Nachrichten immer zuerst zu verschlüsseln und dann zu signieren.

Anwendungsentwickler*innen sollten immer darauf achten, die Anwendbarkeit eines asymmetrischen Signaturschlüssels zu überprüfen, da es keine inhärente Vertrauensbeziehung zwischen einem asymmetrischen Schlüssel und einer beliebigen Nachricht gibt.

Details

In der Vergangenheit gab es einen Konsens darüber, dass wichtige Daten unbedingt mit Methoden wie HMAC- oder RSA-Signaturen verschlüsselt und authentifiziert werden sollten. Es gab jedoch keine eindeutigen Empfehlungen dazu, in welcher Reihenfolge die Verschlüsselungs- und Authentifizierungsvorgänge erfolgen sollen. Aufgrund der in diesem Artikel beschriebenen Sicherheitsrisiken wird von Microsoft nun immer das Paradigma „erst verschlüsseln, dann signieren“ angewendet. Das heißt, dass Daten zuerst mit einem symmetrischen Schlüssel verschlüsselt werden und dann ein MAC oder eine asymmetrische Signatur über den Chiffretext (verschlüsselte Daten) berechnet wird. Beim Entschlüsseln der Daten gehen Sie umgekehrt vor. Bestätigen Sie zunächst den MAC oder die Signatur des Chiffretexts, und entschlüsseln Sie ihn dann.

Als „Padding-Oracle-Angriff“ bekannte Sicherheitsrisiken sind schon seit über zehn Jahren bekannt. Diese Sicherheitsrisiken ermöglichen es Angreifer*innen, Daten zu entschlüsseln, die mit symmetrischen Blockalgorithmen wie AES und 3DES verschlüsselt wurden, wobei nicht mehr als 4096 Versuche pro Datenblock verwendet werden. Bei diesen Sicherheitsrisiken wird die Tatsache ausgenutzt, dass Blockchiffren am häufigsten mit verifizierbaren Auffüllungsdaten am Ende verwendet werden. Es wurde festgestellt, dass Angreifer*innen die Daten entschlüsseln können, wenn sie durch Manipulation des Chiffretexts herausfinden können, wann die Manipulation einen Fehler im Format der Auffüllung am Ende verursacht.

Anfänglich wurden Dienste zum Ziel von Angriffen, die unterschiedliche Fehlercodes zurückgeben, je nachdem, ob die Auffüllung gültig war. Das ist beispielsweise beim ASP.NET-Sicherheitsrisiko MS10-070 der Fall. Microsoft ist nun jedoch der Ansicht, dass es möglich ist, ähnliche Angriffe durchzuführen, indem nur die zeitlichen Unterschiede zwischen der Verarbeitung gültiger und ungültiger Auffüllungen verwendet werden.

Sofern das Verschlüsselungsschema eine Signatur verwendet und die Signaturprüfung mit einer festen Laufzeit für eine bestimmte Datenlänge (unabhängig vom Inhalt) durchgeführt wird, kann die Datenintegrität verifiziert werden, ohne dass Informationen über einen Seitenkanal zu Angreifer*innen gelangen können. Da bei der Integritätsprüfung manipulierte Nachrichten abgelehnt werden, wird die Padding-Oracle-Bedrohung verhindert.

Leitfaden

In erster Linie empfiehlt Microsoft, alle vertraulichen Daten über Transport Layer Security (TLS) zu übertragen, den Nachfolger von Secure Sockets Layer (SSL).

Analysieren Sie als Nächstes Ihre Anwendung:

  • Sie müssen genau wissen, welche Verschlüsselung Sie anwenden und welche Verschlüsselung von den verwendeten Plattformen und APIs unterstützt wird.
  • Sie müssen sich vergewissern, dass bei jeder Verwendung in jeder Schicht eines symmetrischen Blockchiffrealgorithmus (z. B. AES oder 3DES) im CBC-Modus eine Datenintegritätsprüfung mit geheimem Schlüssel stattfindet, zum Beispiel durch eine asymmetrische Signatur, einen HMAC oder die Änderung des Verschlüsselungsmodus in eine authentifizierte Verschlüsselung wie GCM oder CCM.

Basierend auf dem aktuellen Forschungsstand wird allgemein angenommen, dass die Authentifizierung mit Chiffretext (erst verschlüsseln, dann signieren) die beste Option ist, sofern die Authentifizierungs- und Verschlüsselungsschritte bei nicht authentifizierten Verschlüsselungsmodi unabhängig voneinander erfolgen. Es gibt bei der Kryptografie jedoch keinen universellen Ansatz, und eine gezielte Beratung durch Kryptografieexpert*innen sollte dieser Verallgemeinerung vorgezogen werden.

Bei Anwendungen, deren Nachrichtenformat nicht geändert werden kann, aber die eine nicht authentifizierte CBC-Entschlüsselung durchführen, sollten Sie versuchen, Abhilfemaßnahmen wie die folgenden zu integrieren:

  • Entschlüsselung ohne Berechtigung des Entschlüsslers, die Auffüllung zu verifizieren oder zu entfernen:
    • Jegliche Auffüllungen, die angewendet wurden, müssen trotzdem entfernt oder ignoriert werden. Sie übertragen diese Aufgabe auf Ihre Anwendung.
    • Der Vorteil besteht darin, dass die Verifizierung und Entfernung der Auffüllung in die Datenüberprüfungslogik anderer Anwendungen integriert werden können. Wenn die Auffüllungs- und Datenüberprüfung in konstanter Zeit durchgeführt werden können, wird die Bedrohung reduziert.
    • Da die Interpretation der Auffüllung die wahrgenommene Nachrichtenlänge ändert, können bei diesem Ansatz trotzdem Zeitinformationen ausgegeben werden.
  • Änderung des Entschlüsselungsauffüllungsmodus in ISO 10126:
    • Die ISO 10126-Entschlüsselungsauffüllung ist sowohl mit der PKCS7- als auch mit der ANSIX923-Verschlüsselungsauffüllung kompatibel.
    • Wenn Sie den Modus ändern, wird Wissen des Padding Oracle auf ein Byte anstelle des gesamten Blocks reduziert. Wenn der Inhalt jedoch über eine bekannte Fußzeile verfügt, z. B. ein schließendes XML-Element, können ähnliche Angriffe weiterhin den Rest der Nachricht anzielen.
    • Auch die Klartextwiederherstellung wird in Situationen nicht verhindert, in denen Angreifer*innen denselben Klartext mehrmals mit einem anderen Nachrichtenoffset verschlüsseln.
  • Einschränkung der Auswertung eines Entschlüsselungsaufrufs zur Eindämmung des Zeitsignals:
    • Die Berechnung der Haltezeit muss die maximale Dauer um einen bestimmten Mindestbetrag überschreiten, die der Entschlüsselungsvorgang für ein Datensegment mit Auffüllung benötigt.
    • Zeitberechnungen sollten gemäß den Anweisungen unter Abrufen von Zeitstempeln mit hoher Auflösung durchgeführt werden, nicht durch Verwendung von Environment.TickCount (anfällig für Rollover/Überlauf) oder Subtrahieren von zwei Systemzeitstempeln (anfällig für NTP-Anpassungsfehler).
    • Zeitberechnungen müssen den Entschlüsselungsvorgang mit allen potenziellen Ausnahmen in verwalteten oder C++-Anwendungen einschließen und nicht nur auf dem Ende aufgefüllt werden.
    • Wenn bereits ein Erfolg oder Fehler ermittelt wurde, muss das Timinggate einen Fehler zurückgeben, wenn es abläuft.
  • Dienste, die eine nicht authentifizierte Entschlüsselung durchführen, sollten über eine Überwachung verfügen, die erkennt, falls eine Flut von ungültigen Nachrichten auftritt.
    • Beachten Sie, dass dieses Signal sowohl falsch positive Ergebnisse (tatsächlich beschädigte Daten) als auch falsch negative Werte enthält (der Angriff erstreckt sich über eine ausreichend lange Zeit, um sich der Erkennung zu entziehen).

Suche nach anfälligem Code in nativen Anwendungen

Für Programme, die für die CNG-Bibliothek (Cryptography: Next Generation) für Windows entwickelt wurden:

  • Der Entschlüsselungsaufruf erfolgt an BCryptDecrypt, wobei das BCRYPT_BLOCK_PADDING-Flag angegeben wird.
  • Das Schlüsselhandle wurde durch Aufrufen von BCryptSetProperty initialisiert, wobei BCRYPT_CHAINING_MODE auf BCRYPT_CHAIN_MODE_CBC festgelegt ist.
    • Da BCRYPT_CHAIN_MODE_CBC der Standardwert ist, hat der betroffene Code möglicherweise keinen Wert für BCRYPT_CHAINING_MODE zugewiesen.

Für Programme, die für die ältere Kryptografie-API für Windows entwickelt wurden:

  • Der Entschlüsselungsaufruf erfolgt an CryptDecrypt, wobei Final=TRUE angegeben wird.
  • Das Schlüsselhandle wurde durch Aufrufen von CryptSetKeyParam initialisiert, wobei KP_MODE auf CRYPT_MODE_CBC festgelegt ist.
    • Da CRYPT_MODE_CBC der Standardwert ist, hat der betroffene Code möglicherweise keinen Wert für KP_MODE zugewiesen.

Suche nach anfälligem Code in verwalteten Anwendungen

Suche nach anfälligem Code in der Kryptografienachrichtensyntax

Eine nicht authentifizierte CMS-EnvelopedData-Nachricht, deren verschlüsselter Inhalt den CBC-Modus von 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) oder RC2 (1.2.840.113549.3.2) verwendet, ist anfällig, sowie Nachrichten, die andere Blockchiffrealgorithmen im CBC-Modus verwenden.

Streamverschlüsselungsverfahren sind zwar nicht anfällig für dieses spezielle Sicherheitsrisiko, aber Microsoft empfiehlt, die Daten immer zu authentifizieren, statt nur den ContentEncryptionAlgorithm-Wert zu überprüfen.

Für verwaltete Anwendungen kann ein CMS-EnvelopedData-Blob als beliebiger Wert erkannt werden, der an System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]) übergeben wird.

Für native Anwendungen kann ein CMS-EnvelopedData-Blob als beliebiger Wert erkannt werden, der einem CMS-Handle über CryptMsgUpdate bereitgestellt wird, dessen CMSG_TYPE_PARAM den Wert CMSG_ENVELOPED aufweist und/oder dem später eine CMSG_CTRL_DECRYPT-Anweisung über CryptMsgControl gesendet wird.

Beispiel für anfälligen Code (verwaltet)

Diese Methode liest ein Cookie und entschlüsselt es. Es ist keine Datenintegritätsprüfung erkennbar. Daher kann der Inhalt eines Cookies, das von dieser Methode gelesen wird, von den Empfänger*innen oder von Angreifer*innen verwendet werden, die an den verschlüsselten Cookiewert gelangt sind.

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

Der folgende Beispielcode verwendet ein unübliches Nachrichtenformat:

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

Hierbei sind die Algorithmusbezeichner cipher_algorithm_id und hmac_algorithm_id anwendungslokale (nicht dem Standard entsprechende) Repräsentationen dieser Algorithmen. Diese Bezeichner können in anderen Teilen Ihres vorhandenen Messagingprotokolls anstelle eines rein verketteten Bytestreams sinnvoll sein.

In diesem Beispiel wird auch ein einzelner Hauptschlüssel verwendet, um sowohl einen Verschlüsselungsschlüssel als auch einen HMAC-Schlüssel abzuleiten. Das ist einerseits praktisch, um eine Anwendung mit einem Schlüssel in eine Anwendung mit zwei Schlüsseln umzuwandeln, aber auch hilfreich, um dafür zu sorgen, dass beiden Schlüsseln unterschiedliche Werte zugewiesen werden. Außerdem wird sichergestellt, dass der HMAC-Schlüssel und der Verschlüsselungsschlüssel immer synchron bleiben.

Dieses Beispiel akzeptiert Stream weder für die Verschlüsselung noch die Entschlüsselung. Das aktuelle Datenformat erschwert die Verschlüsselung in einem Durchlauf, da der hmac_tag-Wert dem Chiffretext vorangestellt ist. Dieses Format wurde jedoch gewählt, da alle Elemente mit fester Größe am Anfang beibehalten werden, um den Parser einfacher zu halten. Mit diesem Datenformat ist die Entschlüsselung in einem Durchlauf möglich, obwohl ein Implementierer angehalten wird, „GetHashAndReset“ aufzurufen und das Ergebnis vor dem Aufruf von „TransformFinalBlock“ zu überprüfen. Wenn die Streamingverschlüsselung nötig ist, muss möglicherweise ein anderer authentifizierter Verschlüsselungsmodus verwendet werden.

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

Weitere Informationen