CryptoStream FlushFinalBlock 是否保证在密钥错误时抛出异常?
Is CryptoStream FlushFinalBlock GUARANTEED to Throw an Exception When the Key is Wrong?
我知道 CryptoStream
的 FlushFinalBlock
方法在填充错误时抛出,这可能意味着密钥错误。
我想弄清楚的是这是否是测试密钥本身真实性的防弹方法,即 FlushFinalBlock
是否 得到保证 如果给出了错误的键则抛出。或者,换句话说,错误的键是否可能产生不抛出异常但只是乱码输出的结果?如果可能的话,这大概有多大可能? (我专门询问 AES256)。
假设我不知道加密数据是什么并且无法控制其加密,因此除了尝试解密之外我无法验证它。
通常(默认情况下对于 .NET,对于 ECB 和 CBC,CBC 是默认值)使用 PKCS#7 兼容填充。在那种情况下,错误密钥的结果是完全随机化的最终明文块。对于单个填充字节,正确填充的可能性约为 2^256 分之一(接下来的可能性越来越小,65536 分之一等,因此它们相对无关紧要)。一些 unpadding 例程将(错误地)只检查最后一个字节,在这种情况下,机会是 256 中的 16,或 16 中的 1。
对于 PKCS#7,填充 总是 添加。这是有道理的,因为解密例程不知道长度,并且(二进制)消息可能偶然以填充字节结尾 - 正如已经确定的那样。所以你有 16 个字节的值 0x10。由于仍然使用 unpadding,因此创建有效填充的机会当然仍然是 256 分之一,因为 unpadding 例程不知道使用的填充。
要解决填充问题,您需要一个身份验证标签。可以通过在密文上使用 HMAC 来生成该标记 ,同时如果攻击者可以更改 IV,则包括 IV。您还可以使用 GCM 模式,该模式不久前被添加到 .NET 中(即非常非常晚),它既高效又不易出错(您不必明确包含 IV 并且验证更重要或更少)。
请注意,unpadding 的结果也可用于填充 oracle 攻击,这是一种仅通过可用的解密例程创建的明文 oracle 攻击。这些攻击可以通过平均每字节执行 128 次解密操作来完全检索明文。
直接回答这个问题:用错误的密钥解密密文,即加密中未使用的密钥,不是一定会导致异常。
以下 C# 代码显示了使用三个密钥解密密文(密文是使用 AES/CBC 和 PKCS#7 填充生成的):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
...
static void Main(string[] args)
{
byte[] iv = Convert.FromHexString("4B4D907F815EAB0EA3E8A2140968C395");
byte[] ciphertext = Convert.FromHexString("4A9BE2E236868EC0D04F0D280A2876920F79C10969F32D751FF1976E6446F7BBF2469957130E4EE1CC56C386426E1C5C");
// 1.
testCase(1, Convert.FromHexString("5DBF2259D534802EA0C8B24FD6CA876C345AE4C4AE2E000BCB05B33E7465FAEF"), iv, ciphertext);
// 2.
testCase(2, Convert.FromHexString("DFAC3EDECC5F53EF1ACFEE07085A2A5C4327D1057BED2405D5E18EF12DE05C63"), iv, ciphertext);
// 3.
testCase(3, Convert.FromHexString("000102030405060708090A0B0C0D0E0F000102030405060708090A0B0C0D0E0F"), iv, ciphertext);
}
private static void testCase(int num, byte[] key, byte[] iv, byte[] ciphertext)
{
Console.WriteLine("" + num + ".");
try
{
byte[] decryptedNone = Decrypt(ciphertext, key, iv, PaddingMode.None);
byte[] decryptedPkcs7 = Decrypt(ciphertext, key, iv, PaddingMode.PKCS7);
Console.WriteLine("before depadding: " + Convert.ToHexString(decryptedNone));
Console.WriteLine("after depadding: " + Convert.ToHexString(decryptedPkcs7));
Console.WriteLine("UTF-8 decoded: " + Encoding.UTF8.GetString(decryptedPkcs7));
Console.WriteLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static byte[] Decrypt(byte[] cipherText, byte[] Key, byte[] IV, PaddingMode pm)
{
byte[] plaintext = null;
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Key;
aesAlg.IV = IV;
aesAlg.Padding = pm;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (MemoryStream plainTextStream = new MemoryStream())
{
csDecrypt.CopyTo(plainTextStream);
plaintext = plainTextStream.ToArray();
}
}
msDecrypt.Close();
}
}
return plaintext;
}
第一次解密使用了正确的密钥。解密明文的最后 5 个字节是 0x05050505。这符合 PKCS#7,即填充有效并将自动删除。解密成功,明文与原明文相同
第二次解密使用了错误的密钥。碰巧解密明文的最后 2 个字节是 0x0202,这也符合 PKCS#7。因此,此数据被解释为有效填充并自动删除。不会抛出异常。当然解密不成功是因为明文与原来的明文不符
第三次解密也用错了密钥。这里的最后一个字节不符合 PKCS#7。因此,这被解释为无效填充并抛出相应的异常(CryptographicException:填充无效且无法删除)。
因此,输出为:
1.
before depadding: 54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F670505050505
after depadding: 54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67
UTF-8 decoded: The quick brown fox jumps over the lazy dog
2.
before depadding: 4D5B5017ADF3B5EFB5716AB2CCFF8BA6129E35A123287CD564F12C2401AB985EEBC7E274B685595251059C56E2BC0202
after depadding: 4D5B5017ADF3B5EFB5716AB2CCFF8BA6129E35A123287CD564F12C2401AB985EEBC7E274B685595251059C56E2BC
UTF-8 decoded: M[P↨???qj?????↕?5?#(|?d?,$☺??^???t??YRQ♣?V?
3.
Padding is invalid and cannot be removed.
2. 的概率是多少,即错误的键不会导致异常的情况?对于 PKCS#7,填充字节的值对应于填充字节的数量,即对于块大小为 16 字节的 AES,以下填充是可能的,详情请参阅 PKCS#7:
0x01, 0x0202, 0x030303, ..., 0x10101010101010101010101010101010 (full block)
最后一个0x01的概率是265-1,对于0x0202是265-2,以此类推,直到265 -16 为完整块。 IE。尤其是小填充字节对概率的贡献是不可忽略的。
以上概率适用于所有填充字节均已检查的假设(并非所有实现都如此)。如果检查的填充字节较少,则概率会增加,请参阅 .
目前已经考虑了分组密码模式(如CBC),一般都需要强制填充。还有不需要填充的流密码模式(例如 CTR)。对于这些,通常不会因为缺少填充而抛出异常。
正如 中所述和 中详述的那样,经过身份验证的 encryption/decryption(例如 GCM)提供了一种可靠的方法来检测密文是否具有是否被篡改以及是否使用了正确的密钥。
我知道 CryptoStream
的 FlushFinalBlock
方法在填充错误时抛出,这可能意味着密钥错误。
我想弄清楚的是这是否是测试密钥本身真实性的防弹方法,即 FlushFinalBlock
是否 得到保证 如果给出了错误的键则抛出。或者,换句话说,错误的键是否可能产生不抛出异常但只是乱码输出的结果?如果可能的话,这大概有多大可能? (我专门询问 AES256)。
假设我不知道加密数据是什么并且无法控制其加密,因此除了尝试解密之外我无法验证它。
通常(默认情况下对于 .NET,对于 ECB 和 CBC,CBC 是默认值)使用 PKCS#7 兼容填充。在那种情况下,错误密钥的结果是完全随机化的最终明文块。对于单个填充字节,正确填充的可能性约为 2^256 分之一(接下来的可能性越来越小,65536 分之一等,因此它们相对无关紧要)。一些 unpadding 例程将(错误地)只检查最后一个字节,在这种情况下,机会是 256 中的 16,或 16 中的 1。
对于 PKCS#7,填充 总是 添加。这是有道理的,因为解密例程不知道长度,并且(二进制)消息可能偶然以填充字节结尾 - 正如已经确定的那样。所以你有 16 个字节的值 0x10。由于仍然使用 unpadding,因此创建有效填充的机会当然仍然是 256 分之一,因为 unpadding 例程不知道使用的填充。
要解决填充问题,您需要一个身份验证标签。可以通过在密文上使用 HMAC 来生成该标记 ,同时如果攻击者可以更改 IV,则包括 IV。您还可以使用 GCM 模式,该模式不久前被添加到 .NET 中(即非常非常晚),它既高效又不易出错(您不必明确包含 IV 并且验证更重要或更少)。
请注意,unpadding 的结果也可用于填充 oracle 攻击,这是一种仅通过可用的解密例程创建的明文 oracle 攻击。这些攻击可以通过平均每字节执行 128 次解密操作来完全检索明文。
直接回答这个问题:用错误的密钥解密密文,即加密中未使用的密钥,不是一定会导致异常。
以下 C# 代码显示了使用三个密钥解密密文(密文是使用 AES/CBC 和 PKCS#7 填充生成的):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
...
static void Main(string[] args)
{
byte[] iv = Convert.FromHexString("4B4D907F815EAB0EA3E8A2140968C395");
byte[] ciphertext = Convert.FromHexString("4A9BE2E236868EC0D04F0D280A2876920F79C10969F32D751FF1976E6446F7BBF2469957130E4EE1CC56C386426E1C5C");
// 1.
testCase(1, Convert.FromHexString("5DBF2259D534802EA0C8B24FD6CA876C345AE4C4AE2E000BCB05B33E7465FAEF"), iv, ciphertext);
// 2.
testCase(2, Convert.FromHexString("DFAC3EDECC5F53EF1ACFEE07085A2A5C4327D1057BED2405D5E18EF12DE05C63"), iv, ciphertext);
// 3.
testCase(3, Convert.FromHexString("000102030405060708090A0B0C0D0E0F000102030405060708090A0B0C0D0E0F"), iv, ciphertext);
}
private static void testCase(int num, byte[] key, byte[] iv, byte[] ciphertext)
{
Console.WriteLine("" + num + ".");
try
{
byte[] decryptedNone = Decrypt(ciphertext, key, iv, PaddingMode.None);
byte[] decryptedPkcs7 = Decrypt(ciphertext, key, iv, PaddingMode.PKCS7);
Console.WriteLine("before depadding: " + Convert.ToHexString(decryptedNone));
Console.WriteLine("after depadding: " + Convert.ToHexString(decryptedPkcs7));
Console.WriteLine("UTF-8 decoded: " + Encoding.UTF8.GetString(decryptedPkcs7));
Console.WriteLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static byte[] Decrypt(byte[] cipherText, byte[] Key, byte[] IV, PaddingMode pm)
{
byte[] plaintext = null;
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Key;
aesAlg.IV = IV;
aesAlg.Padding = pm;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (MemoryStream plainTextStream = new MemoryStream())
{
csDecrypt.CopyTo(plainTextStream);
plaintext = plainTextStream.ToArray();
}
}
msDecrypt.Close();
}
}
return plaintext;
}
第一次解密使用了正确的密钥。解密明文的最后 5 个字节是 0x05050505。这符合 PKCS#7,即填充有效并将自动删除。解密成功,明文与原明文相同
第二次解密使用了错误的密钥。碰巧解密明文的最后 2 个字节是 0x0202,这也符合 PKCS#7。因此,此数据被解释为有效填充并自动删除。不会抛出异常。当然解密不成功是因为明文与原来的明文不符
第三次解密也用错了密钥。这里的最后一个字节不符合 PKCS#7。因此,这被解释为无效填充并抛出相应的异常(CryptographicException:填充无效且无法删除)。
因此,输出为:
1.
before depadding: 54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F670505050505
after depadding: 54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67
UTF-8 decoded: The quick brown fox jumps over the lazy dog
2.
before depadding: 4D5B5017ADF3B5EFB5716AB2CCFF8BA6129E35A123287CD564F12C2401AB985EEBC7E274B685595251059C56E2BC0202
after depadding: 4D5B5017ADF3B5EFB5716AB2CCFF8BA6129E35A123287CD564F12C2401AB985EEBC7E274B685595251059C56E2BC
UTF-8 decoded: M[P↨???qj?????↕?5?#(|?d?,$☺??^???t??YRQ♣?V?
3.
Padding is invalid and cannot be removed.
2. 的概率是多少,即错误的键不会导致异常的情况?对于 PKCS#7,填充字节的值对应于填充字节的数量,即对于块大小为 16 字节的 AES,以下填充是可能的,详情请参阅 PKCS#7:
0x01, 0x0202, 0x030303, ..., 0x10101010101010101010101010101010 (full block)
最后一个0x01的概率是265-1,对于0x0202是265-2,以此类推,直到265 -16 为完整块。 IE。尤其是小填充字节对概率的贡献是不可忽略的。
以上概率适用于所有填充字节均已检查的假设(并非所有实现都如此)。如果检查的填充字节较少,则概率会增加,请参阅
目前已经考虑了分组密码模式(如CBC),一般都需要强制填充。还有不需要填充的流密码模式(例如 CTR)。对于这些,通常不会因为缺少填充而抛出异常。
正如