关于c#:为什么密码错误会导致“填充无效且无法删除”?

关于c#:为什么密码错误会导致“填充无效且无法删除”?

Why does a bad password cause “Padding is invalid and cannot be removed”?

我需要一些简单的字符串加密,因此我编写了以下代码(此处有很多"启发"):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    // 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());
    }

该代码看起来可以正常工作,除了使用不正确的密钥解密数据时,我在cryptoString中的c.Close()行上收到一个CryptographicException-"填充无效且无法删除"。

示例代码:

1
2
3
4
5
    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(这是一个简单的解释):

  • 给定模数的大小,您可以将k1位填充为0,将k0位填充为随机数。
  • 然后,通过对消息进行某种转换,您将获得加密并发送的填充消息。
  • 这为您提供了对消息的随机化,并提供了一种测试消息是否为垃圾的方法。由于填充方案是可逆的,因此当您解密消息时却无法说出消息本身的完整性,那么您实际上可以对填充进行断言,因此您可以知道消息是否已正确解密或您做错了什么(即有人篡改了消息或您使用了错误的密钥)


    我遇到过类似的"填充无效且无法删除"的情况。例外,但就我而言,关键IV和填充是正确的。

    事实证明,清除加密流是所缺少的。

    像这样:

    1
    2
    3
    4
    5
    6
                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();


    如果希望正确使用,则应在密文中添加身份验证,以便可以验证它是正确的密码还是未修改密文。如果最后一个字节未解密为16个有效填充值(0x01-0x10)之一,则使用ISO10126的填充将仅引发异常。因此,您有1/16的机率不会用错误的密码抛出异常,在这种情况下,如果您对它进行身份验证,则可以采用确定性的方式来判断您的解密是否有效。

    使用crypto api似乎很容易,但实际上很容易出错。例如,您为密钥和iv派生使用了固定的盐,这意味着使用相同密码加密的每个密文都将使用该密钥重用它的IV,这会破坏CBC模式的语义安全性,IV既需要不可预测且唯一给定的密??钥。

    出于容易出错的原因,我有一个代码段,我试图对其进行审查并保持最新(评论,欢迎发布问题):

    字符串C#的对称身份验证加密的现代示例。

    如果使用错误密码时使用AESThenHMAC.AesSimpleDecryptWithPassword(ciphertext, password),则返回null;如果在加密后返回null,则修改了密文或iv,则将永远不会返回垃圾数据或填充异常。


    如果您排除了密钥不匹配的情况,那么除了FlushFinalBlock()(请参阅Yaniv的答案)之外,在CryptoStream上调用Close()也将足够。

    如果要严格使用using块清理资源,请确保将块嵌套为CryptoStream本身:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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();
    }

    我被这个(或类似的)咬住了:

    1
    2
    3
    4
    5
    6
    7
    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中可能有一些未读字节。在完全读取流之前关闭会导致程序出错。


    我有一个类似的问题,解密方法中的问题是初始化一个空的内存流。当我使用密文字节数组初始化它时,它何时起作用:

    1
    MemoryStream ms = new MemoryStream(cipherText)

    用户" atconway"更新的答案对我有用。

    问题不在于填充,而是密钥,在加密和解密过程中密钥是不同的。
    在加密和解密相同值期间,密钥和iv应该相同。


    推荐阅读