Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Microsoft ist der Ansicht, dass es nicht mehr sicher ist, mit dem Verschlüsselungs-Block-Chaining (CBC)-Modus verschlüsselte Daten zu entschlüsseln, wenn nachprüfbares Padding angewendet wurde, ohne zuerst die Integrität des verschlüsselten Textes sicherzustellen, außer in sehr spezifischen Fällen. Dieses Urteil basiert auf der derzeit bekannten kryptografischen Forschung.
Einleitung
Ein Auffüllungs-Oracle-Angriff ist eine Art von Angriff auf verschlüsselte Daten, die es dem Angreifer ermöglicht, den Inhalt der Daten zu entschlüsseln, ohne den Schlüssel zu kennen.
Ein Orakel bezieht sich auf ein "Tell", das einem Angreifer Informationen darüber gibt, ob die von ihm ausgeführte Aktion korrekt ist oder nicht. Stellen Sie sich vor, sie spielen ein Brett- oder Kartenspiel mit einem Kind. Wenn ihr Gesicht mit einem großen Lächeln strahlt, weil sie denken, dass sie kurz davor sind, einen guten Zug zu machen, ist das ein Orakel. Sie können als Gegner dieses Oracle verwenden, um Ihren nächsten Schritt angemessen zu planen.
Padding ist ein bestimmter kryptografischer Begriff. Einige Verschlüsselungen, bei denen es sich um die Algorithmen handelt, die zum Verschlüsseln Ihrer Daten verwendet werden, arbeiten an Datenblöcken, bei denen jeder Block eine feste Größe aufweist. 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 das Padding 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üsselungen haben eine andere Eigenschaft, die als Modus bezeichnet wird, wodurch die Beziehung von Daten im ersten Block zu den Daten im zweiten Block bestimmt wird usw. Einer der am häufigsten verwendeten Modi ist CBC. CBC führt einen anfänglichen zufälligen Block ein, der als Initialisierungsvektor (IV) bezeichnet wird, und kombiniert den vorherigen Block mit dem Ergebnis der statischen Verschlüsselung, damit die Verschlüsselung derselben Nachricht mit demselben Schlüssel nicht immer dieselbe 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. Aus dieser Antwort kann der Angreifer das Bytebyte der Nachricht entschlüsseln.
Moderne Computernetzwerke weisen eine so hohe Qualität auf, dass ein Angreifer sehr kleine (weniger als 0,1 ms) Unterschiede bei der Ausführungszeit auf Remotesystemen erkennen kann. Anwendungen, die davon ausgehen, dass eine erfolgreiche Entschlüsselung nur auftreten kann, wenn die Daten nicht manipuliert wurden, können anfällig für Angriffe von Tools sein, die darauf ausgelegt sind, Unterschiede bei erfolgreicher und erfolgloser Entschlüsselung zu beobachten. Dieser Zeitliche Unterschied kann zwar in einigen Sprachen oder Bibliotheken erheblicher sein als bei anderen, aber es wird angenommen, dass dies eine praktische Bedrohung für alle Sprachen und Bibliotheken ist, wenn die Antwort der Anwendung auf Fehler berücksichtigt wird.
Dieser Angriff basiert auf der Fähigkeit, die verschlüsselten Daten zu ändern und das Ergebnis mit dem Oracle zu testen. Die einzige Möglichkeit, den Angriff vollständig zu mindern, besteht darin, Änderungen an den verschlüsselten Daten zu erkennen und die Ausführung von Aktionen daran zu verweigern. Die Standardmethode hierfür besteht darin, eine Signatur für die Daten zu erstellen und diese Signatur zu überprüfen, bevor Vorgänge ausgeführt werden. Die Signatur muss überprüfbar sein, sie kann nicht vom Angreifer erstellt werden, andernfalls ändern sie die verschlüsselten Daten und berechnen dann eine neue Signatur basierend auf den geänderten Daten. Ein gängiger Signaturtyp wird als Authentifizierungscode für Schlüsselhashnachrichten (Keyed-Hash Message Authentication Code, HMAC) bezeichnet. Ein HMAC unterscheidet sich von einer Prüfsumme, in der er einen geheimen Schlüssel benötigt, der nur der Person bekannt ist, die den HMAC erstellt, und der Person, die ihn überprüft. Ohne Besitz des Schlüssels können Sie keinen korrekten HMAC erzeugen. Wenn Sie Ihre Daten erhalten, nehmen Sie die verschlüsselten Daten, berechnen Sie unabhängig den HMAC mithilfe des geheimen Schlüssels, den Sie und der Absender teilen, und vergleichen Sie dann den HMAC, den sie gesendet haben, mit dem von Ihnen berechneten. Dieser Vergleich muss konstant sein, andernfalls haben Sie ein weiteres auffindbares Oracle hinzugefügt, was einen anderen Angriffstyp zulässt.
Zusammenfassung: Um gepolsterte CBC-Blockchiffre sicher zu verwenden, müssen Sie sie mit einem HMAC (oder einer anderen Datenintegritätsprüfung) kombinieren, die Sie mithilfe eines konstanten Zeitvergleichs überprüfen, bevor Sie versuchen, die Daten zu entschlüsseln. Da alle geänderten Nachrichten die gleiche Zeit in Anspruch nehmen, um eine Antwort zu erzeugen, wird der Angriff verhindert.
Wer anfällig ist
Diese Sicherheitsanfälligkeit gilt sowohl für verwaltete als auch für systemeigene Anwendungen, die ihre eigene Verschlüsselung und Entschlüsselung ausführen. Dazu gehören beispielsweise:
- Eine Anwendung, die ein Cookie für die spätere Entschlüsselung auf dem Server verschlüsselt.
- Eine Datenbankanwendung, die Benutzern die Möglichkeit bietet, Daten in eine Tabelle einzufügen, deren Spalten später entschlüsselt werden.
- Eine Anwendung für die Datenübertragung, die auf Verschlüsselung mit einem freigegebenen Schlüssel basiert, um die Daten während der Übertragung zu schützen.
- Eine Anwendung, die Nachrichten "innerhalb" des TLS-Tunnels verschlüsselt und entschlüsselt.
Beachten Sie, dass Die Verwendung von TLS allein in diesen Szenarien möglicherweise nicht schützt.
Eine anfällige Anwendung:
- entschlüsseln Daten mithilfe des CBC-Verschlüsselungsmodus mit einem verifizierbaren Auffüllungsmodus, z. B. PKCS#7 oder ANSI X.923
- Führt die Entschlüsselung aus, ohne eine Datenintegritätsprüfung durchgeführt zu haben (über einen MAC oder eine asymmetrische digitale Signatur).
Dies gilt auch für Anwendungen, die auf Abstraktionen über diesen Grundtypen basieren, z. B. die Kryptografienachrichtensyntax (PKCS#7/CMS) EnvelopedData-Struktur.
Verwandte Themenbereiche
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. Beispielsweise inhalte, die unter den Regeln der W3C XML-Verschlüsselungssyntax und der Verarbeitungsempfehlung (xmlenc, EncryptedXml) vorbereitet wurden. Während die W3C-Anleitung zum Signieren der Nachricht zum Zeitpunkt als angemessen angesehen wurde, empfiehlt Microsoft jetzt, immer die Verschlüsselung und dann das Signieren durchzuführen.
Anwendungsentwickler sollten immer darauf achten, die Anwendbarkeit eines asymmetrischen Signaturschlüssels zu überprüfen, da es keine inhärente Vertrauensstellung zwischen einem asymmetrischen Schlüssel und einer beliebigen Nachricht gibt.
Einzelheiten
Historisch gesehen besteht ein Konsens darin, dass es wichtig ist, wichtige Daten sowohl zu verschlüsseln als auch zu authentifizieren, z. B. HMAC- oder RSA-Signaturen. Es gab jedoch weniger klare Anleitungen zum Sequenzieren der Verschlüsselungs- und Authentifizierungsvorgänge. Aufgrund der sicherheitsanfälligkeit, die in diesem Artikel beschrieben ist, besteht die Anleitung von Microsoft nun darin, immer das Paradigma "encrypt-then-sign" zu verwenden. Das heißt, daten zuerst mit einem symmetrischen Schlüssel verschlüsseln und dann eine MAC- oder asymmetrische Signatur über den Verschlüsselungstext (verschlüsselte Daten) berechnen. Führen Sie beim Entschlüsseln von Daten die umgekehrte Aktion aus. Bestätigen Sie zuerst den MAC oder die Signatur des Chiffretexts, und entschlüsseln Sie ihn.
Eine Klasse von Sicherheitsrisiken, die als "Padding-Oracle-Angriffe" bekannt sind, ist seit über 10 Jahren bekannt. Diese Sicherheitsrisiken ermöglichen es einem Angreifer, Daten zu entschlüsseln, die durch symmetrische Blockalgorithmen wie AES und 3DES verschlüsselt wurden, wobei maximal 4096 Versuche pro Datenblock verwendet werden. Diese Sicherheitsanfälligkeiten nutzen die Tatsache, dass Blockchiffre am häufigsten mit überprüfbaren Auffüllungsdaten am Ende verwendet werden. Es wurde festgestellt, dass, wenn ein Angreifer verschlüsselungstext manipulieren und herausfinden kann, ob die Manipulation einen Fehler im Format des Abstands am Ende verursacht hat, der Angreifer die Daten entschlüsseln kann.
Anfänglich basierten praktische Angriffe auf Diensten, die unterschiedliche Fehlercodes zurückgeben würden, je nachdem, ob das Padding gültig war, z. B. die ASP.NET-Sicherheitslücke MS10-070. Microsoft ist jedoch nun der Ansicht, dass es praktisch ist, ähnliche Angriffe durchzuführen, die nur die Unterschiede in der Zeitverarbeitung zwischen gültigem und ungültigem Padding verwenden.
Vorausgesetzt, das Verschlüsselungsschema verwendet eine Signatur und dass die Signaturüberprüfung mit einer festen Laufzeit für eine bestimmte Datenlänge (unabhängig vom Inhalt) durchgeführt wird, kann die Datenintegrität überprüft werden, ohne dass informationen an einen Angreifer über einen Seitenkanal gesendet werden. Da die Integritätsprüfung manipulierte Nachrichten ablehnt, wird die Padding Oracle-Bedrohung abgemildert.
Beratung
In erster Linie empfiehlt Microsoft, dass alle Daten mit Vertraulichkeit über TLS (Transport Layer Security), den Nachfolger von Secure Sockets Layer Layer (SSL), übertragen werden müssen.
Analysieren Sie als nächstes Ihre Applikation im Hinblick auf:
- Verstehen Sie genau, welche Verschlüsselung Sie ausführen und welche Verschlüsselung von den plattformen und APIs bereitgestellt wird, die Sie verwenden.
- Stellen Sie sicher, dass jede Verwendung auf jeder Ebene eines symmetrischen Blockchiffrealgorithmus, z. B. AES und 3DES, im CBC-Modus die Verwendung einer Integritätsprüfung mit geheimem Schlüssel umfasst (wie eine asymmetrische Signatur, ein HMAC oder indem der Verschlüsselungsmodus in einen authentifizierten Verschlüsselungsmodus (AE) wie GCM oder CCM geändert wird).
Basierend auf der aktuellen Forschung wird im Allgemeinen angenommen, dass, wenn die Authentifizierungs- und Verschlüsselungsschritte unabhängig für Nicht-AE-Verschlüsselungsmodi durchgeführt werden, die Authentifizierung des Chiffretexts (verschlüsseln-dann-Signieren) die beste allgemeine Option ist. Es gibt jedoch keine richtige Antwort auf Kryptografie, und diese Verallgemeinerung ist nicht so gut wie eine gezielte Beratung von einem professionellen Kryptografen.
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üsseln, ohne dass der Entschlüsseler den Abstand überprüfen oder entfernen kann:
- 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 Überprüfung und Entfernung des Abstands in andere Anwendungsdatenüberprüfungslogik integriert werden kann. 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:
- ISO10126 Entschlüsselungsabstand ist sowohl mit PKCS7-Verschlüsselungsabstand als auch mit ANSIX923 Verschlüsselungsabstand kompatibel.
- Durch das Ändern des Modus wird das Auffüllen von Oracle-Kenntnissen auf 1 Byte anstatt auf den gesamten Block reduziert. Wenn der Inhalt jedoch über eine bekannte Fußzeile verfügt, z. B. ein schließendes XML-Element, können verwandte Angriffe weiterhin den Rest der Nachricht angreifen.
- Dies verhindert auch nicht die Wiederherstellung von Klartext in Situationen, in denen der Angreifer denselben Klartext mehrmals mit einem anderen Nachrichtenoffset verschlüsseln kann.
- Einschränkung der Auswertung eines Entschlüsselungsaufrufs zur Eindämmung des Zeitsignals:
- Die Berechnung der Aufbewahrungszeit muss mindestens die maximale Zeit übersteigen, die der Entschlüsselungsvorgang für alle Datensegmente mit Padding benötigen würde.
- Zeitberechnungen sollten gemäß der Anleitung beim Abrufen von hochauflösenden Zeitstempeln erfolgen, nicht durch Verwendung von Environment.TickCount (da es zu Überläufen/Rollovers kommen kann) oder durch Subtrahieren von zwei Systemzeitstempeln (da es zu NTP-Anpassungsfehlern kommen kann).
- Zeitberechnungen müssen den Entschlüsselungsvorgang und alle potenziellen Ausnahmen in verwalteten oder C++-Anwendungen einbeziehen und dürfen nicht einfach an das Ende angehängt 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, um zu erkennen, dass eine Flut von "ungültigen" Nachrichten durchgekommen ist.
- Denken Sie daran, dass dieses Signal sowohl falsch-positive Ergebnisse (legitim beschädigte Daten) als auch falsch-negative Ergebnisse (Verbreitung des Angriffs über einen ausreichend langen Zeitraum, um die Erkennung zu umgehen) verursacht.
Anfälligen Code finden - native Anwendungen
Für Programme, die mit der Windows-Kryptografie: Next Generation (CNG)-Bibliothek erstellt wurden:
- Der Entschlüsselungsaufruf erfolgt über 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
gesetzt wurde.- Da
BCRYPT_CHAIN_MODE_CBC
die Standardeinstellung ist, hat der betroffene Code möglicherweise keinen Wert zugewiesen fürBCRYPT_CHAINING_MODE
.
- Da
Für Programme, die mit der älteren Windows-Kryptografie-API erstellt 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
gesetzt wurde.- Da
CRYPT_MODE_CBC
die Standardeinstellung ist, hat der betroffene Code möglicherweise keinen Wert zugewiesen fürKP_MODE
.
- Da
Suchen nach anfälligem Code – verwaltete Anwendungen
- Der Entschlüsselungsaufruf erfolgt an die CreateDecryptor()- oder CreateDecryptor(Byte[], Byte[])-Methode in System.Security.Cryptography.SymmetricAlgorithm.
- Dazu gehören die folgenden abgeleiteten Typen innerhalb von .NET, können aber auch Drittanbietertypen enthalten:
- Die SymmetricAlgorithm.Padding Eigenschaft wurde auf PaddingMode.PKCS7, PaddingMode.ANSIX923 oder PaddingMode.ISO10126 gesetzt.
- Da PaddingMode.PKCS7 die Standardeinstellung ist, hat der betroffene Code möglicherweise nie die SymmetricAlgorithm.Padding Eigenschaft zugewiesen.
- Die SymmetricAlgorithm.Mode Eigenschaft wurde auf CipherMode.CBC gesetzt.
- Da CipherMode.CBC die Standardeinstellung ist, hat der betroffene Code möglicherweise nie die SymmetricAlgorithm.Mode Eigenschaft zugewiesen.
Das Suchen anfälligen Codes – Kryptographische-Nachrichtensyntax
Eine nicht authentifizierte CMS EnvelopedData-Nachricht, deren verschlüsselter Inhalt den CBC-Modus von AES verwendet (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) ist anfällig, sowie Nachrichten mit anderen Blockchiffrealgorithmen im CBC-Modus.
Während Datenstromchiffren für diese bestimmte Sicherheitsanfälligkeit nicht anfällig sind, empfiehlt Microsoft, die Daten immer zu authentifizieren, anstatt nur den ContentEncryptionAlgorithm-Wert zu überprüfen.
Bei verwalteten Anwendungen kann ein CMS EnvelopedData-Blob als beliebiger Wert erkannt werden, der an System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]) übergeben wird.
Für systemeigene Anwendungen kann ein CMS EnvelopedData-Blob als beliebiger Wert erkannt werden, der für ein CMS-Handle über CryptMsgUpdate bereitgestellt wird, dessen resultierende CMSG_TYPE_PARAM ist CMSG_ENVELOPED
und/oder der CMS-Handle 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, und es ist keine Datenintegritätsprüfung sichtbar. Daher kann der Inhalt eines Cookies, der von dieser Methode gelesen wird, vom Benutzer, der es erhalten hat, oder von jedem Angreifer, der den verschlüsselten Cookiewert erhalten hat, angegriffen werden.
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();
}
}
Beispielcode nach empfohlenen Methoden – verwaltet
Der folgende Beispielcode verwendet ein nicht standardmäßiges Nachrichtenformat von
cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext
Dabei handelt es sich um cipher_algorithm_id
und hmac_algorithm_id
Algorithmus-Bezeichner, die anwendungsbezogene (nicht standardmäßige) Darstellungen dieser Algorithmen sind. Diese Bezeichner können in anderen Teilen Ihres vorhandenen Messagingprotokolls sinnvoll genutzt werden, statt ausschließlich als nackter, verketteter Bytestrom.
In diesem Beispiel wird auch ein einzelner Masterschlüssel verwendet, um sowohl einen Verschlüsselungsschlüssel als auch einen HMAC-Schlüssel abzuleiten. Dies wird sowohl als Komfort für die Umwandlung einer Einzelschlüssel-Anwendung in eine Dualschlüssel-Anwendung angeboten, als auch um zu fördern, die beiden Schlüssel als unterschiedliche Werte zu bewahren. Außerdem wird sichergestellt, dass der HMAC-Schlüssel und der Verschlüsselungsschlüssel nicht aus der Synchronisierung herauskommen können.
Dieses Beispiel akzeptiert kein Stream für die Verschlüsselung oder 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 ausgewählt, da alle Elemente mit fester Größe am Anfang beibehalten werden, um den Parser einfacher zu halten. Bei diesem Datenformat ist die 1-Pass-Entschlüsselung möglich, obwohl ein Implementierer darauf hingewiesen wird, GetHashAndReset aufzurufen und das Ergebnis vor dem Aufrufen von TransformFinalBlock zu überprüfen. Wenn die Streamingverschlüsselung wichtig ist, ist möglicherweise ein anderer AE-Modus erforderlich.
// ==++==
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Shared under the terms of the Microsoft Public License,
// https://opensource.org/licenses/MS-PL
//
// ==--==
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace Microsoft.Examples.Cryptography
{
public enum AeCipher : byte
{
Unknown,
Aes256CbcPkcs7,
}
public enum AeMac : byte
{
Unknown,
HMACSHA256,
HMACSHA384,
}
/// <summary>
/// Provides extension methods to make HashAlgorithm look like .NET Core's
/// IncrementalHash
/// </summary>
internal static class IncrementalHashExtensions
{
public static void AppendData(this HashAlgorithm hash, byte[] data)
{
hash.TransformBlock(data, 0, data.Length, null, 0);
}
public static void AppendData(
this HashAlgorithm hash,
byte[] data,
int offset,
int length)
{
hash.TransformBlock(data, offset, length, null, 0);
}
public static byte[] GetHashAndReset(this HashAlgorithm hash)
{
hash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return hash.Hash;
}
}
public static partial class AuthenticatedEncryption
{
/// <summary>
/// Use <paramref name="masterKey"/> to derive two keys (one cipher, one HMAC)
/// to provide authenticated encryption for <paramref name="message"/>.
/// </summary>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <param name="message">The message to encrypt</param>
/// <returns>
/// A concatenation of
/// [cipher algorithm+chainmode+padding][mac algorithm][authtag][IV][ciphertext],
/// suitable to be passed to <see cref="Decrypt"/>.
/// </returns>
/// <remarks>
/// <paramref name="masterKey"/> should be a 128-bit (or bigger) value generated
/// by a secure random number generator, such as the one returned from
/// <see cref="RandomNumberGenerator.Create()"/>.
/// This implementation chooses to block deficient inputs by length, but does not
/// make any attempt at discerning the randomness of the key.
///
/// If the master key is being input by a prompt (like a password/passphrase)
/// then it should be properly turned into keying material via a Key Derivation
/// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
/// never be simply turned to bytes via an Encoding class and used as a key.
/// </remarks>
public static byte[] Encrypt(byte[] masterKey, byte[] message)
{
if (masterKey == null)
throw new ArgumentNullException(nameof(masterKey));
if (masterKey.Length < 16)
throw new ArgumentOutOfRangeException(
nameof(masterKey),
"Master Key must be at least 128 bits (16 bytes)");
if (message == null)
throw new ArgumentNullException(nameof(message));
// First, choose an encryption scheme.
AeCipher aeCipher = AeCipher.Aes256CbcPkcs7;
// Second, choose an authentication (message integrity) scheme.
//
// In this example we use the master key length to change from HMACSHA256 to
// HMACSHA384, but that is completely arbitrary. This mostly represents a
// "cryptographic needs change over time" scenario.
AeMac aeMac = masterKey.Length < 48 ? AeMac.HMACSHA256 : AeMac.HMACSHA384;
// It's good to be able to identify what choices were made when a message was
// encrypted, so that the message can later be decrypted. This allows for
// future versions to add support for new encryption schemes, but still be
// able to read old data. A practice known as "cryptographic agility".
//
// This is similar in practice to PKCS#7 messaging, but this uses a
// private-scoped byte rather than a public-scoped Object IDentifier (OID).
// Please note that the scheme in this example adheres to no particular
// standard, and is unlikely to survive to a more complete implementation in
// the .NET Framework.
//
// You may be well-served by prepending a version number byte to this
// message, but may want to avoid the value 0x30 (the leading byte value for
// DER-encoded structures such as X.509 certificates and PKCS#7 messages).
byte[] algorithmChoices = { (byte)aeCipher, (byte)aeMac };
byte[] iv;
byte[] cipherText;
byte[] tag;
// Using our algorithm choices, open an HMAC (as an authentication tag
// generator) and a SymmetricAlgorithm which use different keys each derived
// from the same master key.
//
// A custom implementation may very well have distinctly managed secret keys
// for the MAC and cipher, this example merely demonstrates the master to
// derived key methodology to encourage key separation from the MAC and
// cipher keys.
using (HMAC tagGenerator = GetMac(aeMac, masterKey))
{
using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
using (ICryptoTransform encryptor = cipher.CreateEncryptor())
{
// Since no IV was provided, a random one has been generated
// during the call to CreateEncryptor.
//
// But note that it only does the auto-generation once. If the cipher
// object were used again, a call to GenerateIV would have been
// required.
iv = cipher.IV;
cipherText = Transform(encryptor, message, 0, message.Length);
}
// The IV and ciphertext both need to be included in the MAC to prevent
// tampering.
//
// By including the algorithm identifiers, we have technically moved from
// simple Authenticated Encryption (AE) to Authenticated Encryption with
// Additional Data (AEAD). By including the algorithm identifiers in the
// MAC, it becomes harder for an attacker to change them as an attempt to
// perform a downgrade attack.
//
// If you've added a data format version field, it can also be included
// in the MAC to further inhibit an attacker's options for confusing the
// data processor into believing the tampered message is valid.
tagGenerator.AppendData(algorithmChoices);
tagGenerator.AppendData(iv);
tagGenerator.AppendData(cipherText);
tag = tagGenerator.GetHashAndReset();
}
// Build the final result as the concatenation of everything except the keys.
int totalLength =
algorithmChoices.Length +
tag.Length +
iv.Length +
cipherText.Length;
byte[] output = new byte[totalLength];
int outputOffset = 0;
Append(algorithmChoices, output, ref outputOffset);
Append(tag, output, ref outputOffset);
Append(iv, output, ref outputOffset);
Append(cipherText, output, ref outputOffset);
Debug.Assert(outputOffset == output.Length);
return output;
}
/// <summary>
/// Reads a message produced by <see cref="Encrypt"/> after verifying it hasn't
/// been tampered with.
/// </summary>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <param name="cipherText">
/// The output of <see cref="Encrypt"/>: a concatenation of a cipher ID, mac ID,
/// authTag, IV, and cipherText.
/// </param>
/// <returns>The decrypted content.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="masterKey"/> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="cipherText"/> is <c>null</c>.
/// </exception>
/// <exception cref="CryptographicException">
/// <paramref name="cipherText"/> identifies unknown algorithms, is not long
/// enough, fails a data integrity check, or fails to decrypt.
/// </exception>
/// <remarks>
/// <paramref name="masterKey"/> should be a 128-bit (or larger) value
/// generated by a secure random number generator, such as the one returned from
/// <see cref="RandomNumberGenerator.Create()"/>. This implementation chooses to
/// block deficient inputs by length, but doesn't make any attempt at
/// discerning the randomness of the key.
///
/// If the master key is being input by a prompt (like a password/passphrase),
/// then it should be properly turned into keying material via a Key Derivation
/// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
/// never be simply turned to bytes via an Encoding class and used as a key.
/// </remarks>
public static byte[] Decrypt(byte[] masterKey, byte[] cipherText)
{
// This example continues the .NET practice of throwing exceptions for
// failures. If you consider message tampering to be normal (and thus
// "not exceptional") behavior, you may like the signature
// bool Decrypt(byte[] messageKey, byte[] cipherText, out byte[] message)
// better.
if (masterKey == null)
throw new ArgumentNullException(nameof(masterKey));
if (masterKey.Length < 16)
throw new ArgumentOutOfRangeException(
nameof(masterKey),
"Master Key must be at least 128 bits (16 bytes)");
if (cipherText == null)
throw new ArgumentNullException(nameof(cipherText));
// The format of this message is assumed to be public, so there's no harm in
// saying ahead of time that the message makes no sense.
if (cipherText.Length < 2)
{
throw new CryptographicException();
}
// Use the message algorithm headers to determine what cipher algorithm and
// MAC algorithm are going to be used. Since the same Key Derivation
// Functions (KDFs) are being used in Decrypt as Encrypt, the keys are also
// the same.
AeCipher aeCipher = (AeCipher)cipherText[0];
AeMac aeMac = (AeMac)cipherText[1];
using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
using (HMAC tagGenerator = GetMac(aeMac, masterKey))
{
int blockSizeInBytes = cipher.BlockSize / 8;
int tagSizeInBytes = tagGenerator.HashSize / 8;
int headerSizeInBytes = 2;
int tagOffset = headerSizeInBytes;
int ivOffset = tagOffset + tagSizeInBytes;
int cipherTextOffset = ivOffset + blockSizeInBytes;
int cipherTextLength = cipherText.Length - cipherTextOffset;
int minLen = cipherTextOffset + blockSizeInBytes;
// Again, the minimum length is still assumed to be public knowledge,
// nothing has leaked out yet. The minimum length couldn't just be calculated
// without reading the header.
if (cipherText.Length < minLen)
{
throw new CryptographicException();
}
// It's very important that the MAC be calculated and verified before
// proceeding to decrypt the ciphertext, as this prevents any sort of
// information leaking out to an attacker.
//
// Don't include the tag in the calculation, though.
// First, everything before the tag (the cipher and MAC algorithm ids)
tagGenerator.AppendData(cipherText, 0, tagOffset);
// Skip the data before the tag and the tag, then read everything that
// remains.
tagGenerator.AppendData(
cipherText,
tagOffset + tagSizeInBytes,
cipherText.Length - tagSizeInBytes - tagOffset);
byte[] generatedTag = tagGenerator.GetHashAndReset();
// The time it took to get to this point has so far been a function only
// of the length of the data, or of non-encrypted values (for example, it could
// take longer to prepare the *key* for the HMACSHA384 MAC than the
// HMACSHA256 MAC, but the algorithm choice wasn't a secret).
//
// If the verification of the authentication tag aborts as soon as a
// difference is found in the byte arrays then your program may be
// acting as a timing oracle which helps an attacker to brute-force the
// right answer for the MAC. So, it's very important that every possible
// "no" (2^256-1 of them for HMACSHA256) be evaluated in as close to the
// same amount of time as possible. For this, we call CryptographicEquals
if (!CryptographicEquals(
generatedTag,
0,
cipherText,
tagOffset,
tagSizeInBytes))
{
// Assuming every tampered message (of the same length) took the same
// amount of time to process, we can now safely say
// "this data makes no sense" without giving anything away.
throw new CryptographicException();
}
// Restore the IV into the symmetricAlgorithm instance.
byte[] iv = new byte[blockSizeInBytes];
Buffer.BlockCopy(cipherText, ivOffset, iv, 0, iv.Length);
cipher.IV = iv;
using (ICryptoTransform decryptor = cipher.CreateDecryptor())
{
return Transform(
decryptor,
cipherText,
cipherTextOffset,
cipherTextLength);
}
}
}
private static byte[] Transform(
ICryptoTransform transform,
byte[] input,
int inputOffset,
int inputLength)
{
// Many of the implementations of ICryptoTransform report true for
// CanTransformMultipleBlocks, and when the entire message is available in
// one shot this saves on the allocation of the CryptoStream and the
// intermediate structures it needs to properly chunk the message into blocks
// (since the underlying stream won't always return the number of bytes
// needed).
if (transform.CanTransformMultipleBlocks)
{
return transform.TransformFinalBlock(input, inputOffset, inputLength);
}
// If our transform couldn't do multiple blocks at once, let CryptoStream
// handle the chunking.
using (MemoryStream messageStream = new MemoryStream())
using (CryptoStream cryptoStream =
new CryptoStream(messageStream, transform, CryptoStreamMode.Write))
{
cryptoStream.Write(input, inputOffset, inputLength);
cryptoStream.FlushFinalBlock();
return messageStream.ToArray();
}
}
/// <summary>
/// Open a properly configured <see cref="SymmetricAlgorithm"/> conforming to the
/// scheme identified by <paramref name="aeCipher"/>.
/// </summary>
/// <param name="aeCipher">The cipher mode to open.</param>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <returns>
/// A SymmetricAlgorithm object with the right key, cipher mode, and padding
/// mode; or <c>null</c> on unknown algorithms.
/// </returns>
private static SymmetricAlgorithm GetCipher(AeCipher aeCipher, byte[] masterKey)
{
SymmetricAlgorithm symmetricAlgorithm;
switch (aeCipher)
{
case AeCipher.Aes256CbcPkcs7:
symmetricAlgorithm = Aes.Create();
// While 256-bit, CBC, and PKCS7 are all the default values for these
// properties, being explicit helps comprehension more than it hurts
// performance.
symmetricAlgorithm.KeySize = 256;
symmetricAlgorithm.Mode = CipherMode.CBC;
symmetricAlgorithm.Padding = PaddingMode.PKCS7;
break;
default:
// An algorithm we don't understand
throw new CryptographicException();
}
// Instead of using the master key directly, derive a key for our chosen
// HMAC algorithm based upon the master key.
//
// Since none of the symmetric encryption algorithms currently in .NET
// support key sizes greater than 256-bit, we can use HMACSHA256 with
// NIST SP 800-108 5.1 (Counter Mode KDF) to derive a value that is
// no smaller than the key size, then Array.Resize to trim it down as
// needed.
using (HMAC hmac = new HMACSHA256(masterKey))
{
// i=1, Label=ASCII(cipher)
byte[] cipherKey = hmac.ComputeHash(
new byte[] { 1, 99, 105, 112, 104, 101, 114 });
// Resize the array to the desired keysize. KeySize is in bits,
// and Array.Resize wants the length in bytes.
Array.Resize(ref cipherKey, symmetricAlgorithm.KeySize / 8);
symmetricAlgorithm.Key = cipherKey;
}
return symmetricAlgorithm;
}
/// <summary>
/// Open a properly configured <see cref="HMAC"/> conforming to the scheme
/// identified by <paramref name="aeMac"/>.
/// </summary>
/// <param name="aeMac">The message authentication mode to open.</param>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <returns>
/// An HMAC object with the proper key, or <c>null</c> on unknown algorithms.
/// </returns>
private static HMAC GetMac(AeMac aeMac, byte[] masterKey)
{
HMAC hmac;
switch (aeMac)
{
case AeMac.HMACSHA256:
hmac = new HMACSHA256();
break;
case AeMac.HMACSHA384:
hmac = new HMACSHA384();
break;
default:
// An algorithm we don't understand
throw new CryptographicException();
}
// Instead of using the master key directly, derive a key for our chosen
// HMAC algorithm based upon the master key.
// Since the output size of the HMAC is the same as the ideal key size for
// the HMAC, we can use the master key over a fixed input once to perform
// NIST SP 800-108 5.1 (Counter Mode KDF):
hmac.Key = masterKey;
// i=1, Context=ASCII(MAC)
byte[] newKey = hmac.ComputeHash(new byte[] { 1, 77, 65, 67 });
hmac.Key = newKey;
return hmac;
}
// A simple helper method to ensure that the offset (writePos) always moves
// forward with new data.
private static void Append(byte[] newData, byte[] combinedData, ref int writePos)
{
Buffer.BlockCopy(newData, 0, combinedData, writePos, newData.Length);
writePos += newData.Length;
}
/// <summary>
/// Compare the contents of two arrays in an amount of time which is only
/// dependent on <paramref name="length"/>.
/// </summary>
/// <param name="a">An array to compare to <paramref name="b"/>.</param>
/// <param name="aOffset">
/// The starting position within <paramref name="a"/> for comparison.
/// </param>
/// <param name="b">An array to compare to <paramref name="a"/>.</param>
/// <param name="bOffset">
/// The starting position within <paramref name="b"/> for comparison.
/// </param>
/// <param name="length">
/// The number of bytes to compare between <paramref name="a"/> and
/// <paramref name="b"/>.</param>
/// <returns>
/// <c>true</c> if both <paramref name="a"/> and <paramref name="b"/> have
/// sufficient length for the comparison and all of the applicable values are the
/// same in both arrays; <c>false</c> otherwise.
/// </returns>
/// <remarks>
/// An "insufficient data" <c>false</c> response can happen early, but otherwise
/// a <c>true</c> or <c>false</c> response take the same amount of time.
/// </remarks>
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static bool CryptographicEquals(
byte[] a,
int aOffset,
byte[] b,
int bOffset,
int length)
{
Debug.Assert(a != null);
Debug.Assert(b != null);
Debug.Assert(length >= 0);
int result = 0;
if (a.Length - aOffset < length || b.Length - bOffset < length)
{
return false;
}
unchecked
{
for (int i = 0; i < length; i++)
{
// Bitwise-OR of subtraction has been found to have the most
// stable execution time.
//
// This cannot overflow because bytes are 1 byte in length, and
// result is 4 bytes.
// The OR propagates all set bytes, so the differences are only
// present in the lowest byte.
result = result | (a[i + aOffset] - b[i + bOffset]);
}
}
return result == 0;
}
}
}