Luki w zabezpieczeniach chronometrażu z odszyfrowywaniem symetrycznym w trybie CBC przy użyciu uzupełnienia
Firma Microsoft uważa, że nie jest już bezpieczne odszyfrowywanie danych zaszyfrowanych przy użyciu trybu szyfrowania blokowego (CBC) szyfrowania symetrycznego w przypadku zastosowania weryfikowalnego wypełnienia bez uprzedniego zapewnienia integralności szyfrowanego tekstu, z wyjątkiem bardzo konkretnych okoliczności. Ten wyrok opiera się na obecnie znanych badaniach kryptograficznych.
Wprowadzenie
Atak wyroczni dopełniania to typ ataku na zaszyfrowane dane, który umożliwia osobie atakującej odszyfrowywanie zawartości danych bez znajomości klucza.
Wyrocznia odwołuje się do "tell", która daje atakującemu informacje o tym, czy wykonywana akcja jest poprawna, czy nie. Wyobraź sobie, że gra na planszy lub karcie z dzieckiem. Kiedy ich twarz zapala się z wielkim uśmiechem, ponieważ myślą, że mają na celu dobry ruch, to wyrocznia. Jako przeciwnik możesz użyć tej wyroczni, aby odpowiednio zaplanować następny ruch.
Dopełnienie to określony termin kryptograficzny. Niektóre szyfry, które są algorytmami używanymi do szyfrowania danych, działają na blokach danych, w których każdy blok jest stałym rozmiarem. Jeśli dane, które chcesz zaszyfrować, nie są właściwym rozmiarem do wypełnienia bloków, dane są wypełniane do momentu, gdy nie zostanie on wypełniony. Wiele form wypełnienia wymaga, aby dopełnienie zawsze było obecne, nawet jeśli oryginalne dane wejściowe były odpowiedniego rozmiaru. Dzięki temu dopełnienie będzie zawsze bezpiecznie usuwane po odszyfrowaniu.
Łącząc te dwie elementy, implementacja oprogramowania z wyrocznią dopełniającą ujawnia, czy odszyfrowane dane mają prawidłowe wypełnienie. Wyrocznia może być tak prosta, jak zwracanie wartości, która mówi "Invalid padding" lub coś bardziej skomplikowanego, takiego jak biorąc zupełnie inny czas, aby przetworzyć prawidłowy blok, w przeciwieństwie do nieprawidłowego bloku.
Szyfry oparte na blokach mają inną właściwość o nazwie tryb, który określa relację danych w pierwszym bloku z danymi w drugim bloku itd. Jednym z najczęściej używanych trybów jest CBC. CBC wprowadza początkowy blok losowy, znany jako wektor inicjowania (IV) i łączy poprzedni blok z wynikiem szyfrowania statycznego, dzięki czemu szyfrowanie tego samego komunikatu przy użyciu tego samego klucza nie zawsze generuje te same zaszyfrowane dane wyjściowe.
Osoba atakująca może użyć wyroczni uzupełniania w połączeniu ze strukturą danych CBC, aby wysyłać nieco zmienione komunikaty do kodu, który uwidacznia wyrocznię i wysyłać dane, dopóki wyrocznia nie poinformuje ich, że dane są poprawne. Z tej odpowiedzi osoba atakująca może odszyfrować bajty wiadomości bajtami.
Nowoczesne sieci komputerowe są tak wysokiej jakości, że osoba atakująca może wykryć bardzo małe (mniej niż 0,1 ms) różnice w czasie wykonywania w systemach zdalnych. Aplikacje, które zakładają, że pomyślne odszyfrowywanie może wystąpić tylko wtedy, gdy dane nie zostały naruszone, mogą być narażone na ataki z narzędzi, które zostały zaprojektowane w celu obserwowania różnic w pomyślnym i nieudanym odszyfrowaniu. Chociaż ta różnica czasu może być bardziej znacząca w niektórych językach lub bibliotekach niż inne, uważa się teraz, że jest to praktyczne zagrożenie dla wszystkich języków i bibliotek, gdy uwzględniana jest odpowiedź aplikacji na błąd.
Ten atak polega na możliwości zmiany zaszyfrowanych danych i przetestowaniu wyniku za pomocą wyroczni. Jedynym sposobem całkowitego wyeliminowania ataku jest wykrycie zmian w zaszyfrowanych danych i odmowę wykonania na nim jakichkolwiek akcji. Standardowym sposobem wykonania tej czynności jest utworzenie podpisu dla danych i zweryfikowanie tego podpisu przed wykonaniem jakichkolwiek operacji. Podpis musi być weryfikowalny, nie można go utworzyć przez osobę atakującą, w przeciwnym razie zmienić zaszyfrowane dane, a następnie obliczyć nowy podpis na podstawie zmienionych danych. Jednym z typowych typów odpowiedniego podpisu jest znany jako kod uwierzytelniania komunikatu skrótu klucza (HMAC). HMAC różni się od sumy kontrolnej, ponieważ przyjmuje klucz tajny, znany tylko osobie produkującej HMAC i osobie sprawdzającej ją. Bez posiadania klucza nie można utworzyć poprawnego klucza HMAC. Po otrzymaniu danych należy pobrać zaszyfrowane dane, niezależnie obliczyć klucz HMAC przy użyciu klucza tajnego użytkownika i udziału nadawcy, a następnie porównać dane HMAC wysyłane z obliczoną wartością. To porównanie musi być stałym czasem, w przeciwnym razie dodano kolejną wykrywalną wyrocznię umożliwiającą atak innego typu.
Podsumowując, aby bezpiecznie używać wyściełanych szyfrów blokowych CBC, należy połączyć je z HMAC (lub innym sprawdzaniem integralności danych), które należy zweryfikować przy użyciu porównania stałego czasu przed próbą odszyfrowania danych. Ponieważ wszystkie zmienione komunikaty zajmują tyle samo czasu, aby utworzyć odpowiedź, atak jest blokowany.
KtoTo jest podatna na zagrożenia
Ta luka w zabezpieczeniach dotyczy aplikacji zarządzanych i natywnych, które wykonują własne szyfrowanie i odszyfrowywanie. Obejmuje to na przykład:
- Aplikacja, która szyfruje plik cookie do późniejszego odszyfrowywania na serwerze.
- Aplikacja bazy danych, która umożliwia użytkownikom wstawianie danych do tabeli, której kolumny są później odszyfrowywane.
- Aplikacja do transferu danych, która opiera się na szyfrowaniu przy użyciu klucza współużytkowanego w celu ochrony przesyłanych danych.
- Aplikacja, która szyfruje i odszyfrowuje komunikaty "wewnątrz" tunelu TLS.
Należy pamiętać, że używanie protokołu TLS może nie chronić Cię w tych scenariuszach.
Aplikacja podatna na zagrożenia:
- Odszyfrowuje dane przy użyciu trybu szyfrowania CBC z weryfikowalnym trybem dopełnienia, takim jak PKCS#7 lub ANSI X.923.
- Wykonuje odszyfrowywanie bez przeprowadzania kontroli integralności danych (za pośrednictwem komputera MAC lub asymetrycznego podpisu cyfrowego).
Dotyczy to również aplikacji opartych na abstrakcji na podstawie tych elementów pierwotnych, takich jak kryptograficzna składnia komunikatów (PKCS#7/CMS) KopertaData.
Powiązane obszary zainteresowania
Badania doprowadziły firmę Microsoft do dalszego zaniepokojenia komunikatami CBC, które są wyposażone w dopełnienie równoważne iso 10126, gdy komunikat ma dobrze znaną lub przewidywalną strukturę stopki. Na przykład zawartość przygotowana zgodnie z regułami zalecenia dotyczącego składni i przetwarzania szyfrowania XML W3C (xmlenc, EncryptedXml). Podczas gdy wskazówki dotyczące W3C podpisywania komunikatu następnie szyfrowanie zostało uznane za odpowiednie w tym czasie, firma Microsoft zaleca teraz zawsze wykonywanie szyfrowania i podpisywania.
Deweloperzy aplikacji powinni zawsze pamiętać o weryfikowaniu stosowania klucza podpisu asymetrycznego, ponieważ nie ma żadnej relacji zaufania między kluczem asymetrycznym a dowolnym komunikatem.
Szczegóły
W przeszłości istniała zgoda, że ważne jest zarówno szyfrowanie, jak i uwierzytelnianie ważnych danych przy użyciu środków, takich jak podpisy HMAC lub RSA. Jednak istnieją mniej jasne wskazówki dotyczące sekwencji operacji szyfrowania i uwierzytelniania. Ze względu na lukę w zabezpieczeniach opisano w tym artykule, wskazówki firmy Microsoft mają teraz zawsze używać paradygmatu "szyfruj następnie podpisywania". Oznacza to, że najpierw szyfruj dane przy użyciu klucza symetrycznego, a następnie obliczysz klucz MAC lub sygnaturę asymetryczną za pośrednictwem szyfrowanego tekstu (zaszyfrowanych danych). Podczas odszyfrowywania danych wykonaj odwrotnie. Najpierw potwierdź adres MAC lub podpis szyfrującego tekstu, a następnie odszyfruj go.
Od ponad 10 lat istnieje klasa luk w zabezpieczeniach znana jako "ataki wyroczni dopełniania". Te luki w zabezpieczeniach umożliwiają atakującemu odszyfrowywanie danych zaszyfrowanych przez algorytmy bloków symetrycznych, takich jak AES i 3DES, przy użyciu nie więcej niż 4096 prób na blok danych. Te luki w zabezpieczeniach wykorzystują fakt, że szyfry blokowe są najczęściej używane z weryfikowalnymi danymi dopełniania na końcu. Stwierdzono, że jeśli osoba atakująca może manipulować tekstem szyfrowania i dowiedzieć się, czy manipulacja spowodowała błąd w formacie wypełnienia na końcu, osoba atakująca może odszyfrować dane.
Początkowo praktyczne ataki były oparte na usługach, które zwracały różne kody błędów na podstawie tego, czy wypełnienie było prawidłowe, takie jak luka w zabezpieczeniach ASP.NET MS10-070. Jednak firma Microsoft uważa teraz, że praktyczne jest przeprowadzanie podobnych ataków przy użyciu tylko różnic w czasie między prawidłowym i nieprawidłowym wypełnieniem przetwarzania.
Pod warunkiem, że schemat szyfrowania korzysta z podpisu i że weryfikacja podpisu jest przeprowadzana przy użyciu stałego środowiska uruchomieniowego dla danej długości danych (niezależnie od zawartości), integralność danych można zweryfikować bez emitowania żadnych informacji do osoby atakującej za pośrednictwem kanału bocznego. Ponieważ sprawdzanie integralności odrzuca wszelkie naruszone komunikaty, zagrożenie wyrocznią uzupełniania jest ograniczane.
Wskazówki
Przede wszystkim firma Microsoft zaleca, aby wszelkie dane, które mają poufność, muszą być przesyłane za pośrednictwem protokołu Transport Layer Security (TLS), następcy protokołu Secure Sockets Layer (SSL).
Następnie przeanalizuj aplikację pod kątem:
- Dokładnie dowiedz się, jakie szyfrowanie wykonujesz, oraz jakie szyfrowanie jest udostępniane przez używane platformy i interfejsy API.
- Należy się upewnić, że każde użycie w każdej warstwie algorytmu szyfrowania bloków symetrycznych, takich jak AES i 3DES, w trybie CBC obejmuje użycie kontroli integralności danych kluczy tajnych (sygnatury asymetrycznej, HMAC lub zmiany trybu szyfrowania na tryb szyfrowania uwierzytelnionego (AE), takiego jak GCM lub CCM.
W oparciu o bieżące badania powszechnie uważa się, że jeśli kroki uwierzytelniania i szyfrowania są wykonywane niezależnie dla trybów szyfrowania innych niż AE, uwierzytelnianie szyfrowania (szyfrowanie następnie podpisywanie) jest najlepszą ogólną opcją. Nie ma jednak żadnej poprawnej odpowiedzi na kryptografię, a ta uogólnienie nie jest tak dobra, jak porady skierowane od profesjonalnego kryptografii.
Zachęcamy aplikacje, które nie mogą zmienić formatu obsługi komunikatów, ale nieuwierzytelnione odszyfrowywanie CBC, aby spróbować uwzględnić środki zaradcze, takie jak:
- Odszyfruj bez zezwolenia programowi odszyfrowywania na weryfikowanie lub usuwanie wypełnienia:
- Wszelkie zastosowane dopełnienie nadal musi zostać usunięte lub zignorowane. Przenosisz obciążenie do aplikacji.
- Zaletą jest możliwość włączenia weryfikacji i usunięcia wypełnienia do innego logiki weryfikacji danych aplikacji. Jeśli weryfikacja wypełnienia i weryfikacja danych można przeprowadzić w stałym czasie, zagrożenie zostanie zmniejszone.
- Ponieważ interpretacja wypełnienia zmienia postrzeganą długość komunikatu, nadal mogą istnieć informacje o chronometrażu emitowane z tego podejścia.
- Zmień tryb wypełnienia odszyfrowywania na ISO10126:
- ISO10126 wypełnienie odszyfrowywania jest zgodne zarówno z dopełnieniem szyfrowania PKCS7, jak i ANSIX923 dopełnieniem szyfrowania.
- Zmiana trybu zmniejsza wiedzę wyroczni dopełniania do 1 bajtu zamiast całego bloku. Jeśli jednak zawartość zawiera dobrze znaną stopkę, taką jak zamykający element XML, powiązane ataki mogą nadal atakować pozostałą część komunikatu.
- Nie zapobiega to również odzyskiwaniu zwykłego tekstu w sytuacjach, w których osoba atakująca może wielokrotnie szyfrować ten sam zwykły tekst z innym przesunięciem komunikatu.
- Brama oceny wywołania odszyfrowywania w celu tłumienia sygnału chronometrażu:
- Obliczanie czasu wstrzymania musi mieć minimalny czas przekraczający maksymalny czas operacji odszyfrowywania dla dowolnego segmentu danych zawierającego dopełnienie.
- Obliczenia czasowe należy wykonać zgodnie ze wskazówkami zawartymi w artykule Uzyskiwanie sygnatur czasowych o wysokiej rozdzielczości, a nie przy użyciu Environment.TickCount (z zastrzeżeniem przerzucania/przepełnienia) lub odjęciu dwóch sygnatur czasowych systemu (z zastrzeżeniem błędów korekt NTP).
- Obliczenia czasowe muszą uwzględniać operację odszyfrowywania, w tym wszystkie potencjalne wyjątki w zarządzanych aplikacjach lub C++, a nie tylko na końcu.
- Jeśli jeszcze określono powodzenie lub niepowodzenie, brama chronometrażu musi zwrócić błąd po wygaśnięciu.
- Usługi, które wykonują nieuwierzytelnione odszyfrowywanie, powinny mieć monitorowanie w celu wykrycia, że doszło do powodzi "nieprawidłowych" komunikatów.
- Należy pamiętać, że ten sygnał niesie ze sobą zarówno fałszywie dodatnie (legalnie uszkodzone dane), jak i fałszywie ujemne (rozprzestrzenianie ataku na wystarczająco długi czas w celu uniknięcia wykrycia).
Znajdowanie kodu podatnego na zagrożenia — aplikacje natywne
W przypadku programów skompilowanych w bibliotece kryptografii systemu Windows: Następna generacja (CNG):
- Wywołanie odszyfrowywania to BCryptDecrypt, określając flagę
BCRYPT_BLOCK_PADDING
. - Uchwyt klucza został zainicjowany przez wywołanie elementu BCryptSetProperty z BCRYPT_CHAINING_MODE ustawioną na wartość
BCRYPT_CHAIN_MODE_CBC
.- Ponieważ
BCRYPT_CHAIN_MODE_CBC
jest to wartość domyślna, kod, którego dotyczy problem, może nie mieć przypisanej żadnej wartości dla elementuBCRYPT_CHAINING_MODE
.
- Ponieważ
W przypadku programów utworzonych przy użyciu starszego interfejsu API kryptograficznego systemu Windows:
- Wywołanie odszyfrowywania to CryptDecrypt za pomocą
Final=TRUE
polecenia . - Dojście klucza zostało zainicjowane przez wywołanie metody CryptSetKeyParam z KP_MODE ustawioną na wartość
CRYPT_MODE_CBC
.- Ponieważ
CRYPT_MODE_CBC
jest to wartość domyślna, kod, którego dotyczy problem, może nie mieć przypisanej żadnej wartości dla elementuKP_MODE
.
- Ponieważ
Znajdowanie kodu podatnego na zagrożenia — aplikacje zarządzane
- Wywołanie odszyfrowywania dotyczy CreateDecryptor() metod lub CreateDecryptor(Byte[], Byte[]) w pliku System.Security.Cryptography.SymmetricAlgorithm.
- Obejmuje to następujące typy pochodne na platformie .NET, ale mogą również obejmować typy innych firm:
- Właściwość została ustawiona SymmetricAlgorithm.Padding na PaddingMode.PKCS7, PaddingMode.ANSIX923lub PaddingMode.ISO10126.
- Ponieważ PaddingMode.PKCS7 jest to wartość domyślna, kod, którego dotyczy problem, nigdy nie miał przypisanej SymmetricAlgorithm.Padding właściwości.
- Właściwość została ustawiona SymmetricAlgorithm.Mode na wartość CipherMode.CBC
- Ponieważ CipherMode.CBC jest to wartość domyślna, kod, którego dotyczy problem, nigdy nie miał przypisanej SymmetricAlgorithm.Mode właściwości.
Znajdowanie kodu podatnego na zagrożenia — składnia komunikatów kryptograficznych
Nieuwierzytelniony komunikat CMS EnvelopedData, którego zaszyfrowana zawartość używa trybu CBC 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 (3DES) 1.2.840.113549.3.7) lub RC2 (1.2.840.113549.3.2) jest narażony, a także komunikaty korzystające z innych algorytmów szyfrowania blokowego w trybie CBC.
Chociaż szyfry strumieniowe nie są podatne na tę konkretną lukę w zabezpieczeniach, firma Microsoft zaleca zawsze uwierzytelnianie danych za pośrednictwem inspekcji wartości ContentEncryptionAlgorithm.
W przypadku aplikacji zarządzanych obiekt blob CMS EnvelopedData można wykryć jako dowolną wartość przekazywaną do System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[])obiektu .
W przypadku aplikacji natywnych obiekt blob CMS EnvelopedData można wykryć jako dowolną wartość dostarczoną do dojścia CMS za pośrednictwem narzędzia CryptMsgUpdate, którego wynikowa CMSG_TYPE_PARAM jest CMSG_ENVELOPED
i/lub uchwyt CMS jest później wysyłany instrukcję CMSG_CTRL_DECRYPT
za pośrednictwem narzędzia CryptMsgControl.
Przykład kodu podatnego na zagrożenia — zarządzany
Ta metoda odczytuje plik cookie i odszyfrowuje go, a sprawdzanie integralności danych nie jest widoczne. W związku z tym zawartość pliku cookie odczytywanego przez tę metodę może zostać zaatakowana przez użytkownika, który go otrzymał, lub przez osobę atakującą, która uzyskała zaszyfrowaną wartość pliku cookie.
private byte[] DecryptCookie(string cookieName)
{
HttpCookie cookie = Request.Cookies[cookieName];
if (cookie == null)
{
return null;
}
using (ICryptoTransform decryptor = _aes.CreateDecryptor())
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream =
new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
{
byte[] readCookie = Convert.FromBase64String(cookie.Value);
cryptoStream.Write(readCookie, 0, readCookie.Length);
cryptoStream.FlushFinalBlock();
return memoryStream.ToArray();
}
}
Przykładowy kod zgodnie z zalecanymi rozwiązaniami — zarządzany
Poniższy przykładowy kod używa standardowego formatu komunikatu
cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext
cipher_algorithm_id
gdzie identyfikatory i hmac_algorithm_id
są reprezentacjami algorytmów lokalnych (niestandardowych) tych algorytmów. Te identyfikatory mogą mieć sens w innych częściach istniejącego protokołu obsługi komunikatów zamiast jako bez połączenia bytestream.
W tym przykładzie użyto również pojedynczego klucza głównego do uzyskania zarówno klucza szyfrowania, jak i klucza HMAC. Jest to zarówno wygoda przekształcania aplikacji singly-keyed w aplikację z podwójnym kluczem, jak i zachęcanie do przechowywania dwóch kluczy jako różnych wartości. Dodatkowo gwarantuje, że klucz HMAC i klucz szyfrowania nie mogą wydostać się z synchronizacji.
Ten przykład nie akceptuje Stream szyfrowania ani odszyfrowywania. Bieżący format danych sprawia, że szyfrowanie jednoprzepustowe jest trudne, ponieważ hmac_tag
wartość poprzedza tekst szyfrowania. Jednak ten format został wybrany, ponieważ przechowuje wszystkie elementy o stałym rozmiarze na początku, aby zachować prostszy analizator. W przypadku tego formatu danych możliwe jest odszyfrowywanie jednoprzepustowe, chociaż implementator jest ostrzegany, aby wywołać metodę GetHashAndReset i zweryfikować wynik przed wywołaniem polecenia TransformFinalBlock. Jeśli szyfrowanie strumieniowe jest ważne, może być wymagany inny tryb AE.
// ==++==
//
// 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;
}
}
}