为什么密码错误会导致“填充无效且无法删除”?
-
08-06-2019 - |
题
我需要一些简单的字符串加密,所以我编写了以下代码(有很多“灵感”来自 这里):
// create and initialize a crypto algorithm
private static SymmetricAlgorithm getAlgorithm(string password) {
SymmetricAlgorithm algorithm = Rijndael.Create();
Rfc2898DeriveBytes rdb = new Rfc2898DeriveBytes(
password, new byte[] {
0x53,0x6f,0x64,0x69,0x75,0x6d,0x20, // salty goodness
0x43,0x68,0x6c,0x6f,0x72,0x69,0x64,0x65
}
);
algorithm.Padding = PaddingMode.ISO10126;
algorithm.Key = rdb.GetBytes(32);
algorithm.IV = rdb.GetBytes(16);
return algorithm;
}
/*
* encryptString
* provides simple encryption of a string, with a given password
*/
public static string encryptString(string clearText, string password) {
SymmetricAlgorithm algorithm = getAlgorithm(password);
byte[] clearBytes = System.Text.Encoding.Unicode.GetBytes(clearText);
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, algorithm.CreateEncryptor(), CryptoStreamMode.Write);
cs.Write(clearBytes, 0, clearBytes.Length);
cs.Close();
return Convert.ToBase64String(ms.ToArray());
}
/*
* decryptString
* provides simple decryption of a string, with a given password
*/
public static string decryptString(string cipherText, string password) {
SymmetricAlgorithm algorithm = getAlgorithm(password);
byte[] cipherBytes = Convert.FromBase64String(cipherText);
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, algorithm.CreateDecryptor(), CryptoStreamMode.Write);
cs.Write(cipherBytes, 0, cipherBytes.Length);
cs.Close();
return System.Text.Encoding.Unicode.GetString(ms.ToArray());
}
该代码似乎工作正常,除了当使用不正确的密钥解密数据时,我在解密字符串中的 cs.Close() 行上收到 CryptographicException - “填充无效且无法删除”。
示例代码:
string password1 = "password";
string password2 = "letmein";
string startClearText = "The quick brown fox jumps over the lazy dog";
string cipherText = encryptString(startClearText, password1);
string endClearText = decryptString(cipherText, password2); // exception thrown
我的问题是,这是可以预料的吗?我本以为使用错误的密码解密只会导致无意义的输出,而不是异常。
解决方案
虽然这已经得到回答,但我认为解释一下是个好主意 为什么 这是可以预料的。
通常应用填充方案,因为大多数加密过滤器在语义上并不安全,并且无法防止某些形式的加密攻击。例如,通常在 RSA 中 OAEP 使用填充方案可以防止某些类型的攻击(例如选择的明文攻击或 致盲).
填充方案在发送消息之前将一些(通常)随机垃圾附加到消息 m 中。以OAEP方式为例,使用了两个Oracle(这是一个简单的解释):
- 给定模数的大小,您可以用 0 填充 k1 位,用随机数填充 k0 位。
- 然后,通过对消息进行一些转换,您将获得加密并发送的填充消息。
这为您提供了消息的随机化以及测试消息是否是垃圾的方法。由于填充方案是可逆的,当您解密消息时,虽然您无法透露消息本身的完整性,但实际上您可以对填充进行一些断言,从而知道消息是否已被正确解密或者您做错了什么(即有人篡改了消息或您使用了错误的密钥)
其他提示
我经历了类似的“填充无效,无法删除”。例外,但是就我而言,钥匙IV和填充是正确的。
事实证明,所缺少的只是刷新加密流。
像这样:
MemoryStream msr3 = new MemoryStream();
CryptoStream encStream = new CryptoStream(msr3, RijndaelAlg.CreateEncryptor(), CryptoStreamMode.Write);
encStream.Write(bar2, 0, bar2.Length);
// unless we flush the stream we would get "Padding is invalid and cannot be removed." exception when decoding
encStream.FlushFinalBlock();
byte[] bar3 = msr3.ToArray();
如果你希望你的用法正确,你应该添加 验证 到您的密文,以便您可以验证密码是否正确或密文未被修改。您正在使用的填充 ISO10126 仅当最后一个字节未解密为 16 个有效填充值 (0x01-0x10) 之一时,才会引发异常。因此,您有 1/16 的机会不会使用错误的密码抛出异常,如果您对其进行身份验证,您就可以确定性地判断您的解密是否有效。
使用加密API虽然看似简单,但实际上很容易犯错误。例如,您使用固定盐来进行密钥和 iv 派生,这意味着使用相同密码加密的每个密文都将使用该密钥重用其 IV,这会破坏 CBC 模式的语义安全性,IV 需要既不可预测又是唯一的给定的密钥。
由于容易犯错误,我有一个代码片段,我尝试对其进行审查和更新(欢迎评论、问题):
如果你使用它 AESThenHMAC.AesSimpleDecryptWithPassword(ciphertext, password)
当使用错误的密码时, null
如果密文或 iv 在加密后被修改,则返回 null
返回后,您将永远不会得到垃圾数据或填充异常。
如果您已经排除了键不匹配的情况,那么此外 FlushFinalBlock()
(参见 Yaniv 的回答),打电话 Close()
于 CryptoStream
也足够了。
如果您严格清理资源 using
块,请确保嵌套该块 CryptoStream
本身:
using (MemoryStream ms = new MemoryStream())
using (var enc = RijndaelAlg.CreateEncryptor())
{
using (CryptoStream encStream = new CryptoStream(ms, enc, CryptoStreamMode.Write))
{
encStream.Write(bar2, 0, bar2.Length);
} // implicit close
byte[] encArray = ms.ToArray();
}
我被这个(或类似的)咬过:
using (MemoryStream ms = new MemoryStream())
using (var enc = RijndaelAlg.CreateEncryptor())
using (CryptoStream encStream = new CryptoStream(ms, enc, CryptoStreamMode.Write))
{
encStream.Write(bar2, 0, bar2.Length);
byte[] encArray = ms.ToArray();
} // implicit close -- too late!
是的,这是可以预料的,或者至少,这正是当我们的加密例程获取不可解密数据时发生的情况
异常的另一个原因可能是使用解密逻辑的多个线程之间的竞争条件 - ICryptoTransform 的本机实现是 不是线程安全的 (例如。SymmetricAlgorithm),因此应将其放在排他部分,例如使用 锁。请参考这里了解更多详情: http://www.make-awesome.com/2011/07/system-security-cryptography-and-thread-safety/
CryptoStream 中可能存在一些未读取的字节。在完全读取流之前关闭导致我的程序出现错误。
我遇到了类似的问题,解密方法中的问题是初始化空内存流。当我使用密文字节数组初始化它时,它起作用了,如下所示:
MemoryStream ms = new MemoryStream(cipherText)
用户“atconway”更新的答案对我有用。
问题不在于填充,而在于加密和解密期间的密钥不同。加密和解密相同值时,密钥和 iv 应该相同。