Уязвимости в учете времени при симметричной расшифровке в режиме CBC с использованием заполнения
Корпорация Майкрософт считает, что более небезопасно расшифровывать данные, зашифрованные с помощью режима симметричного шифрования шифров (CBC), если проверяемое заполнение было применено, не обеспечивая целостность шифра, за исключением очень конкретных обстоятельств. Это решение основано на известных в настоящее время криптографических исследований.
Введение
Атака oracle с заполнением — это тип атаки на зашифрованные данные, которые позволяют злоумышленнику расшифровывать содержимое данных, не зная ключа.
Оракул ссылается на "сообщить", который предоставляет злоумышленнику сведения о том, правильно ли выполняется действие. Представьте себе, что играйте в доску или карта игру с ребенком. Когда их лицо осветится большой улыбкой, потому что они думают, что они собираются сделать хороший шаг, это оракул. Вы, как противник, можете использовать этот оракул для планирования следующего шага соответствующим образом.
Заполнение — это конкретный криптографический термин. Некоторые шифры, которые являются алгоритмами, используемыми для шифрования данных, работают над блоками данных, где каждый блок является фиксированным размером. Если данные, которые вы хотите зашифровать, не нужный размер для заполнения блоков, данные будут заполнены до тех пор, пока они не будут выполняться. Во многих формах заполнения требуется всегда присутствовать заполнение, даже если исходный вход был правильным размером. Это позволяет всегда безопасно удалять заполнение при расшифровке.
Объединение двух вещей, реализация программного обеспечения с заполнением oracle показывает, имеет ли расшифровка данных допустимую заполнение. Оракул может быть чем-то простым, как возврат значения, которое говорит "Недопустимое заполнение" или что-то более сложное, как принимать измеримое другое время для обработки допустимого блока в отличие от недопустимого блока.
Шифры на основе блоков имеют другое свойство, называемое режимом, которое определяет связь данных в первом блоке с данными во втором блоке и т. д. Одним из наиболее часто используемых режимов является CBC. CBC представляет начальный случайный блок, известный как вектор инициализации (IV), и объединяет предыдущий блок с результатом статического шифрования, чтобы сделать его таким, чтобы шифрование того же сообщения с тем же ключом не всегда приводило к тому же зашифрованному выводу.
Злоумышленник может использовать оракул заполнения в сочетании с структурой данных CBC, чтобы отправлять несколько измененные сообщения в код, предоставляющий oracle, и продолжать отправлять данные, пока oracle не сообщит им, что данные правильны. В этом ответе злоумышленник может расшифровать байт сообщения байтами.
Современные компьютерные сети имеют такое высокое качество, что злоумышленник может обнаруживать очень небольшие (менее 0,1 мс) различия во времени выполнения в удаленных системах. Приложения, предполагающие, что успешное расшифровка может произойти только в том случае, если данные не были изменены, могут быть уязвимы для атаки со стороны средств, которые предназначены для наблюдения за различиями в успешной и неудачной расшифровке. Хотя эта разница во времени может быть более значительной на некоторых языках или библиотеках, чем другие, теперь считается, что это практическая угроза для всех языков и библиотек, когда ответ приложения на сбой учитывается.
Эта атака зависит от возможности изменить зашифрованные данные и проверить результат с помощью oracle. Единственным способом полностью устранить атаку является обнаружение изменений зашифрованных данных и отказ выполнять какие-либо действия с ним. Стандартным способом этого является создание подписи для данных и проверка подписи перед выполнением любых операций. Подпись должна быть проверена, она не может быть создана злоумышленником, в противном случае они изменят зашифрованные данные, а затем вычисляют новую подпись на основе измененных данных. Один из распространенных типов соответствующей подписи называется кодом проверки подлинности хэш-сообщений с ключом (HMAC). HMAC отличается от проверка sum в том, что он принимает секретный ключ, известный только человеку, создающему HMAC, и человеку, проверяющему его. Без владения ключом вы не можете создать правильный HMAC. При получении данных вы будете принимать зашифрованные данные, независимо вычислить HMAC с помощью секретного ключа, который вы и общей папке отправителя, а затем сравнить HMAC, отправляемый с вычисляемой. Это сравнение должно быть постоянным временем, в противном случае вы добавили еще один обнаруживаемый oracle, что позволяет использовать другой тип атаки.
В итоге для безопасного использования блочных шифров CBC необходимо объединить их с HMAC (или другой проверка целостности данных), который проверяется с помощью сравнения времени констант, прежде чем пытаться расшифровать данные. Так как все измененные сообщения занимают одинаковое время для создания ответа, атака предотвращается.
Кто уязвим
Эта уязвимость применяется как к управляемым, так и к собственным приложениям, выполняющим собственное шифрование и расшифровку. Это включает в себя, например:
- Приложение, которое шифрует файл cookie для последующей расшифровки на сервере.
- Приложение базы данных, которое предоставляет пользователям возможность вставлять данные в таблицу, столбцы которых позже расшифровываются.
- Приложение для передачи данных, использующее шифрование с помощью общего ключа для защиты передаваемых данных.
- Приложение, которое шифрует и расшифровывает сообщения внутри туннеля TLS.
Обратите внимание, что использование TLS может не защитить вас в этих сценариях.
Уязвимое приложение:
- Расшифровывает данные с помощью режима шифра CBC с проверяемым режимом заполнения, например PKCS#7 или ANSI X.923.
- Выполняет расшифровку без выполнения целостности данных проверка (с помощью MAC или асимметричной цифровой подписи).
Это также относится к приложениям, созданным на основе абстракций поверх этих примитивов, таких как синтаксис криптографических сообщений (PKCS#7/CMS) StructuredData.
Связанные области озабоченности
Исследования привели к тому, что корпорация Майкрософт будет более обеспокоена сообщениями CBC, которые заполнены iso 10126-эквивалентным заполнением, когда сообщение имеет известную или прогнозируемую структуру нижнего колонтитула. Например, содержимое, подготовленное в соответствии с правилами синтаксиса и обработки XML W3C (xmlenc, EncryptedXml). Хотя руководство по W3C для подписи сообщения, то шифрование считается подходящим в то время, корпорация Майкрософт теперь рекомендует всегда выполнять шифрование, а затем подписывать.
Разработчики приложений всегда должны учитывать применимость асимметричного ключа подписи, так как между асимметричным ключом доверия и произвольным сообщением нет связи доверия.
Сведения
Исторически существует консенсус, что важно шифровать и проверять подлинность важных данных, используя такие средства, как HMAC или RSA сигнатуры. Однако было менее ясное руководство по последовательности операций шифрования и проверки подлинности. Из-за уязвимости, подробно описанной в этой статье, руководство Майкрософт теперь всегда использует парадигму "encrypt-then-sign". То есть сначала шифруйте данные с помощью симметричного ключа, а затем вычислите mac-подпись или асимметричную подпись по шифру (зашифрованные данные). При расшифровке данных выполните обратный процесс. Сначала подтвердите MAC или подпись зашифрованного текста, а затем расшифруйте его.
Класс уязвимостей, известных как "заполнения атак oracle", как известно, существует более 10 лет. Эти уязвимости позволяют злоумышленнику расшифровывать данные, зашифрованные алгоритмами симметричного блока, например AES и 3DES, используя не более 4096 попыток на блок данных. Эти уязвимости используют тот факт, что блочные шифры чаще всего используются с проверяемыми данными о заполнении в конце. Было обнаружено, что если злоумышленник может изменить зашифрованный текст и выяснить, вызвана ли ошибка в формате заполнения в конце, злоумышленник может расшифровать данные.
Первоначально практические атаки основывались на службах, которые возвращали различные коды ошибок на основе допустимой заполнений, таких как уязвимость MS10-070 ASP.NET. Тем не менее, корпорация Майкрософт теперь считает, что практически рекомендуется проводить аналогичные атаки, используя только различия во времени обработки допустимых и недопустимых заполнений.
При условии, что схема шифрования использует подпись, и что проверка подписи выполняется с фиксированной средой выполнения для заданной длины данных (независимо от содержимого), целостность данных может быть проверена без получения каких-либо сведений злоумышленнику через боковой канал. Так как целостность проверка отклоняет любые измененные сообщения, угроза оракула заполнения устраняется.
Руководство
В первую очередь корпорация Майкрософт рекомендует передавать все данные, которые имеют конфиденциальность, по протоколу TLS, преемнику протокола SSL.
Затем проанализируйте приложение следующим образом:
- Точно понять, какое шифрование выполняется и какой шифрование предоставляется платформами и API, которые вы используете.
- Убедитесь, что каждое использование на каждом уровне алгоритма шифра симметричного блока, например AES и 3DES, в режиме CBC включает использование проверка целостности данных с ключом секрета (асимметричная подпись, HMAC или изменение режима шифра на режим проверки подлинности (AE), например GCM или CCM.
На основе текущих исследований обычно считается, что при выполнении действий проверки подлинности и шифрования независимо для режимов шифрования, отличных от AE, проверка подлинности шифра (шифрование и подпись) является лучшим вариантом. Тем не менее, нет единого размера правильный ответ на криптографию, и эта обобщение не так хорошо, как направленные советы от профессионального криптографа.
Приложения, которые не могут изменить формат обмена сообщениями, но выполнять расшифровку CBC без проверки подлинности, рекомендуется попытаться включить такие способы устранения рисков, как:
- Расшифровка без разрешения расшифровки для проверки или удаления заполнений:
- Любое примененное заполнение по-прежнему необходимо удалить или игнорировать, вы перемещаете нагрузку в приложение.
- Преимущество заключается в том, что проверка и удаление заполнений можно включить в другую логику проверки данных приложения. Если проверка заполнения и проверка данных могут выполняться в постоянное время, угроза уменьшается.
- Так как интерпретация заполнения изменяет воспринимаемую длину сообщения, может по-прежнему быть информация о времени, выдаваемая этим подходом.
- Измените режим расшифровки на ISO10126:
- ISO10126 расшифровка совместима с заполнением шифрования PKCS7 и ANSIX923 заполнением шифрования.
- Изменение режима уменьшает объем знаний oracle на 1 байт, а не весь блок. Однако если содержимое имеет известный нижний колонтитул, например закрывающий XML-элемент, связанные атаки могут продолжать атаковать остальную часть сообщения.
- Это также не позволяет предотвратить восстановление обычного текста в ситуациях, когда злоумышленник может принуждать один и тот же открытый текст к шифрованию несколько раз с другим смещением сообщения.
- Ворот оценка вызова расшифровки, чтобы ослабить сигнал времени:
- Вычисление времени удержания должно иметь минимальное значение, превышающее максимальное время операции расшифровки для любого сегмента данных, содержащего заполнение.
- Вычисления времени должны выполняться в соответствии с рекомендациями по получению меток времени высокого разрешения, а не с помощью Environment.TickCount (при накате или переполнении) или вычитания двух системных меток времени (при условии ошибок корректировки NTP).
- Вычисления времени должны включать операцию расшифровки, включая все потенциальные исключения в управляемых приложениях или приложениях C++, а не только в конце.
- Если успешное выполнение или сбой было определено еще, шлюз времени должен возвращать сбой после истечения срока действия.
- Службы, выполняющие расшифровку без проверки подлинности, должны иметь мониторинг, чтобы определить, что произошло наводнение "недопустимых" сообщений.
- Имейте в виду, что этот сигнал несет как ложные срабатывания (законно поврежденные данные), так и ложные отрицательные (распространение атаки в течение достаточно длительного времени, чтобы избежать обнаружения).
Поиск уязвимого кода — собственные приложения
Для программ, созданных на основе библиотеки Windows Cryptography: Next Generation (CNG):
- Вызов расшифровки — это BCryptDecrypt, задающий
BCRYPT_BLOCK_PADDING
флаг. - Дескриптор ключа инициализирован путем вызова BCryptSetProperty с BCRYPT_CHAINING_MODE задано значение
BCRYPT_CHAIN_MODE_CBC
.- Так как
BCRYPT_CHAIN_MODE_CBC
это значение по умолчанию, затронутый код, возможно, не назначил никакое значениеBCRYPT_CHAINING_MODE
.
- Так как
Для программ, созданных на основе более старого API шифрования Windows:
- Вызов расшифровки — это CryptDecrypt с
Final=TRUE
. - Дескриптор ключа инициализирован путем вызова CryptSetKeyParam с KP_MODE задано значение
CRYPT_MODE_CBC
.- Так как
CRYPT_MODE_CBC
это значение по умолчанию, затронутый код, возможно, не назначил никакое значениеKP_MODE
.
- Так как
Поиск уязвимого кода — управляемые приложения
- Вызов расшифровки — это CreateDecryptor() методы или CreateDecryptor(Byte[], Byte[]) методы System.Security.Cryptography.SymmetricAlgorithm.
- К ним относятся следующие производные типы в .NET, но также могут включать сторонние типы:
- Для SymmetricAlgorithm.Padding свойства задано PaddingMode.PKCS7значение , PaddingMode.ANSIX923или PaddingMode.ISO10126.
- Так как PaddingMode.PKCS7 это значение по умолчанию, затронутый код, возможно, никогда не назначил свойство SymmetricAlgorithm.Padding .
- Для SymmetricAlgorithm.Mode свойства задано значение CipherMode.CBC
- Так как CipherMode.CBC это значение по умолчанию, затронутый код, возможно, никогда не назначил свойство SymmetricAlgorithm.Mode .
Поиск уязвимого кода — синтаксис криптографического сообщения
Неуправляемое сообщение CMS EnvelopedData, зашифрованное содержимое которого использует режим AES (2.16.840.1.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) или RC2 (1.2.840.113549.3.2) уязвим, а также сообщения, использующие любые другие алгоритмы шифрования блоков в режиме CBC.
Хотя шифры потоков не подвержены этой конкретной уязвимости, корпорация Майкрософт рекомендует всегда выполнять проверку подлинности данных при проверке значения ContentEncryptionAlgorithm.
Для управляемых приложений большой двоичный объект CMS EnvelopedData можно обнаружить как любое значение, переданное System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[])в .
Для собственных приложений большой двоичный объект CMS EnvelopedData можно обнаружить как любое значение, предоставленное дескриптору CMS через CryptMsgUpdate , результатом которого CMSG_TYPE_PARAM является CMSG_ENVELOPED
и /или дескриптор CMS позже отправляет инструкцию CMSG_CTRL_DECRYPT
через CryptMsgControl.
Пример уязвимого кода — управляемый
Этот метод считывает файл cookie и расшифровывает его и не отображается проверка целостности данных. Таким образом, содержимое файла cookie, считываемого этим методом, может быть атакован пользователем, получившим его, или любым злоумышленником, который получил зашифрованное значение 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();
}
}
Пример кода, приведенный ниже рекомендуемых рекомендаций— управляемый
В следующем примере кода используется формат нестандартного сообщения
cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext
cipher_algorithm_id
где идентификаторы и hmac_algorithm_id
идентификаторы алгоритма являются локальными (нестандартными) представлениями этих алгоритмов. Эти идентификаторы могут содержать смысл в других частях существующего протокола обмена сообщениями, а не в виде сцепленного байтового потока.
В этом примере также используется один главный ключ для получения ключа шифрования и ключа HMAC. Это предоставляется как для удобства, так и для того, чтобы превратить приложение с поддержкой singly keyed в приложение с двумя ключами и обеспечить сохранение двух ключей в качестве разных значений. Кроме того, он гарантирует, что ключ HMAC и ключ шифрования не могут выйти из синхронизации.
Этот пример не принимает Stream шифрование или расшифровку. Текущий формат данных затрудняет шифрование с одним проходом, так как hmac_tag
значение предшествует зашифрованному тексту. Однако этот формат был выбран, так как он сохраняет все элементы фиксированного размера в начале, чтобы упростить синтаксический анализ. С помощью этого формата данных возможна однопроходная расшифровка, хотя реализующий предостережен, чтобы вызвать GetHashAndReset и проверить результат перед вызовом TransformFinalBlock. Если шифрование потоковой передачи важно, может потребоваться другой режим 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;
}
}
}