使用填充对 CBC 模式对称解密的漏洞进行计时
Microsoft 认为,除非是非常特殊的情况,否则在应用可验证填充但未首先确保密文完整性的情况下,解密使用对称加密的密码块链 (CBC) 模式加密的数据不再安全。 这种判断是基于当前已知的密码学研究。
简介
填充预言机攻击是一种针对加密数据的攻击,能让攻击者在不知道密钥的情况下解密数据的内容。
预言机指的是“告知”,它为攻击者提供有关他们正在执行的操作是否正确的信息。 想象一下和孩子玩棋盘或纸牌游戏。 当他们的脸上露出灿烂的笑容,因为他们认为自己将要下一手好棋时,那就是神谕。 作为对手,你可以使用这个神谕来适当地计划你的下一步行动。
填充是一个特定的加密术语。 一些密码是用于加密数据的算法,适用于每个块大小固定的数据块。 如果你要加密的数据大小不适合填充块,则会填充你的数据,直到适合块。 许多形式的填充要求填充始终存在,即使原始输入的大小正确。 这允许在解密时始终安全地删除填充。
将这两件事放在一起,带有填充预言机的软件实现揭示了解密的数据是否具备有效的填充。 预言机的简繁程度不一,简单的就是返回一个“无效填充”的值,复杂的将花费不同程度的时间来处理有效块(与无效块相反)。
基于块的密码有另一个属性,称为模式,模式确定第一个块中的数据与第二个块中的数据的关系,依此类推。 最常用的模式之一是 CBC。 CBC 引入了一个初始随机块,称为初始化向量 (IV),并将前一个块与静态加密的结果相结合,因此使用相同密钥加密相同消息并不总是产生相同的加密输出。
攻击者可以使用填充预言机,再结合 CBC 数据的结构,便可以向暴露预言机的代码发送稍微改变的消息,并继续发送数据,直到预言机告诉他们数据是正确的。 从这个响应中,攻击者可以逐字节解密消息。
现代计算机网络的质量非常之高,以至于攻击者可以在远程系统上检测到非常小的(小于 0.1 毫秒)执行时间差异。 旨在观察成功解密和不成功解密差异的工具的攻击目标很可能是那些假设只有在数据没有被篡改的情况下才能成功解密的应用程序。 虽然这种时间差异在某些语言或库中可能比其他语言或库更重要,但现在人们认为,当考虑到应用程序对故障的响应时,这对所有语言和库来说都是一个实际威胁。
这种攻击依赖于更改加密数据并使用预言机测试结果的能力。 完全缓解攻击的唯一方法是检测加密数据的更改并拒绝对其执行任何操作。 执行此操作的标准方法是为数据创建签名并在执行任何操作之前验证该签名。 签名必须是可验证的,不能由攻击者创建,否则攻击者会更改加密数据,然后根据更改的数据计算新的签名。 一种常见的适当签名类型称为密钥散列消息认证码 (HMAC)。 HMAC 与校验和的不同之处在于,HMAC 需要一个密钥,只有生成 HMAC 的人和验证它的人才知道。 如果没有密钥,你将无法生成正确的 HMAC。 在你收到数据时,你将获取加密数据,使用你和发送者共享的密钥独立计算 HMAC,然后将他们发送的 HMAC 与你计算的 HMAC 进行比较。 这种比较必须是恒定的时间,否则你添加了另一个可检测的预言机,从而允许不同类型的攻击。
总之,要安全地使用填充的 CBC 分组加密,你必须将它们与你在尝试解密数据之前使用恒定时间比较验证的 HMAC(或其他数据完整性检查)结合起来。 由于所有更改的消息都需要相同的时间来产生响应,因此可以防止攻击。
谁容易遭受攻击
此漏洞适用于执行自己的加密和解密的托管应用程序和本机应用程序。 例如,这包括:
- 加密 cookie 以便稍后在服务器上解密的应用程序。
- 一种数据库应用程序,使用户能够将数据插入到表中,表中的列稍后会被解密。
- 一种数据传输应用程序,依赖于使用共享密钥的加密来保护传输中的数据。
- 在 TLS 隧道“内部”加密和解密消息的应用程序。
请注意,在这些情况下,单独使用 TLS 可能无法保护你。
易受攻击的应用程序:
- 使用具有可验证填充模式(例如 PKCS#7 或 ANSI X.923)的 CBC 密码模式解密数据。
- 在未执行数据完整性检查(通过 MAC 或非对称数字签名)的情况下执行解密。
这也适用于在这些原语之上的抽象之上构建的应用程序,例如加密消息语法 (PKCS#7/CMS) EnvelopedData 结构。
相关关注领域
这些研究让 Microsoft 进一步关注在消息具有众所周知或可预测的页脚结构时使用 ISO 10126 等效填充而填充的 CBC 消息。 例如,根据 W3C XML 加密语法和处理建议 (xmlenc, EncryptedXml) 的规则准备的内容。 虽然当时认为 W3C 对消息进行签名然后加密的指南是合适的,但 Microsoft 现在建议始终执行“先加密后签名”。
应用程序开发人员应始终注意验证非对称签名密钥的适用性,因为非对称密钥和任意消息之间没有固有的信任关系。
详细信息
从历史上看,人们一致认为,使用 HMAC 或 RSA 签名等手段对重要数据进行加密和验证都很重要。 但是,关于如何对加密和身份验证操作进行排序的指导还没有明确。 由于本文中详述的漏洞,Microsoft 现在的指导是始终使用“加密然后签名”范例。 也就是说,首先使用对称密钥加密数据,然后在密文(加密数据)上计算 MAC 或非对称签名。 解密数据时,执行相反的操作。 首先确认密文的 MAC 或签名,然后解密。
已知存在 10 多年的称为“填充预言机攻击”的一类漏洞。 这些漏洞让攻击者能够解密由对称块算法(如 AES 和 3DES)加密的数据,每个数据块使用不超过 4096 次尝试。 这些漏洞利用了分组加密最常与最后可验证的填充数据一起使用这一事实。 之前就已经发现,如果攻击者能够篡改密文,并找出篡改是否导致最后填充的格式错误,攻击者就可以解密数据。
最初,实际攻击基于服务,这些服务会根据填充是否有效返回不同的错误代码,例如 ASP.NET 漏洞 MS10-070。 但是,Microsoft 现在认为,仅使用处理有效和无效填充之间的时间差异来进行类似的攻击是可行的。
如果加密方案采用了签名,并且对给定长度的数据(与内容无关)在固定的运行时间下进行签名验证,则可以在不通过侧通道向攻击者发送任何信息的情况下验证数据的完整性。 由于完整性检查拒绝任何被篡改的消息,因此缓解了填充预言机威胁。
指南
首先,Microsoft 建议任何具有机密性的数据都需要通过传输层安全性 (TLS) 传输,TLS 是安全套接字层 (SSL) 的延续。
接下来,分析你的应用程序以进行以下操作:
- 准确了解你正在执行的加密以及你正在使用的平台和 API 提供的加密。
- 确保在 CBC 模式下,对称分组加密算法(如 AES 和 3DES)的每一层的每次使用都包含使用密钥数据完整性检查(非对称签名、HMAC 或将密码模式更改为认证加密 (AE) 模式,例如 GCM 或 CCM)。
根据目前的研究,一般认为当非 AE 加密模式的认证和加密步骤独立执行时,对密文进行认证(先加密后签名)是最好的通用选择。 然而,对于密码学来说,并没有万能的正确答案,而且这种概括并不像专业密码学家的直接建议那么好。
鼓励无法更改其消息格式但执行未经身份验证的 CBC 解密的应用程序将尝试合并缓解措施,例如:
- 在不允许解密器验证或删除填充的情况下进行解密:
- 应用的任何填充仍需要删除或忽略,你将负担转移到应用程序中。
- 好处是填充验证和删除可以合并到其他应用程序数据验证逻辑中。 如果填充验证和数据验证可以在恒定时间内完成,则威胁将降低。
- 由于填充的解释改变了感知到的消息长度,因此这种方法可能仍然会发出时间信息。
- 将解密填充模式更改为 ISO10126:
- ISO10126 解密填充兼容 PKCS7 加密填充和 ANSIX923 加密填充。
- 更改模式会将填充预言机知识减少到 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
来说,受影响的代码可能没有被分配以任何值。
- 由于
对于针对旧版 Windows Cryptographic API 构建的程序:
- 解密调用是针对 CryptDecrypt(采用
Final=TRUE
)。 - 密钥句柄已通过调用 CryptSetKeyParam 并将 KP_MODE 设置为
CRYPT_MODE_CBC
进行了初始化。- 由于
CRYPT_MODE_CBC
是默认设置,因此对于KP_MODE
来说,受影响的代码可能没有被分配以任何值。
- 由于
查找易受攻击的代码托管应用程序
- 解密调用是针对 System.Security.Cryptography.SymmetricAlgorithm 上的 CreateDecryptor() 或 CreateDecryptor(Byte[], Byte[]) 方法。
- 这包括 .NET 中的以下派生类型,但也可能包括第三方类型:
- SymmetricAlgorithm.Padding 属性已设置为 PaddingMode.PKCS7、PaddingMode.ANSIX923 或 PaddingMode.ISO10126。
- 由于 PaddingMode.PKCS7 是默认设置,因此受影响的代码可能永远不会分配 SymmetricAlgorithm.Padding 属性。
- SymmetricAlgorithm.Mode 属性已设置为 CipherMode.CBC
- 由于 CipherMode.CBC 是默认设置,因此受影响的代码可能永远不会分配 SymmetricAlgorithm.Mode 属性。
查找易受攻击的代码 - 加密消息语法
未经身份验证的 CMS EnvelopedData 消息,其加密内容使用 AES 的 CBC 模式(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) 或 RC2 (1.2.840.113549.3.2) 以及在 CBC 模式下使用任何其他分组加密算法的消息都是易受攻击的。
虽然流密码不易受此特定漏洞的影响,但 Microsoft 建议始终对数据进行身份验证,而不是检查 ContentEncryptionAlgorithm 值。
对于托管应用程序,可以将 CMS EnvelopedData blob 检测为传递给 System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[])。
对于本机应用程序,CMS EnvelopedData blob 可以被检测为通过 CryptMsgUpdate 提供给 CMS 句柄的任何值,其生成的 CMSG_TYPE_PARAM 为 CMSG_ENVELOPED
且/或 CMS 句柄稍后通过 CryptMsgControl 发送 CMSG_CTRL_DECRYPT
指令。
易受攻击的代码示例 - 托管
此方法读取 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 密钥。 这既是为了方便将单键控应用程序转换为双键控应用程序,也是为了鼓励将两个键保持为不同的值。 这进一步保证了 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;
}
}
}