为什么使用AesCryptoServiceProvider第二次解密时该字符串不相等?

3

我在C#(VS2012,.NET 4.5)的文本加密和解密方面遇到了问题。具体来说,当我加密并随后解密字符串时,输出结果与输入不同。然而,奇怪的是,如果我复制加密输出并将其硬编码为字符串文字,则解密可行。以下代码示例说明了问题。我做错了什么?

var key = new Rfc2898DeriveBytes("test password", Encoding.Unicode.GetBytes("test salt"));
var provider = new AesCryptoServiceProvider { Padding = PaddingMode.PKCS7, KeySize = 256 };
var keyBytes = key.GetBytes(provider.KeySize >> 3);
var ivBytes = key.GetBytes(provider.BlockSize >> 3);
var encryptor = provider.CreateEncryptor(keyBytes, ivBytes);
var decryptor = provider.CreateDecryptor(keyBytes, ivBytes);

var testStringBytes = Encoding.Unicode.GetBytes("test string");
var testStringEncrypted = Convert.ToBase64String(encryptor.TransformFinalBlock(testStringBytes, 0, testStringBytes.Length));

//Prove that the encryption has resulted in the following string
Debug.WriteLine(testStringEncrypted == "cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc="); //Result: True

//Decrypt the encrypted text from a hardcoded string literal
var encryptedBytes = Convert.FromBase64String("cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc=");
var testStringDecrypted = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length));

//Decrypt the encrypted text from the string result of the encryption process
var encryptedBytes2 = Convert.FromBase64String(testStringEncrypted);
var testStringDecrypted2 = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes2, 0, encryptedBytes2.Length));

//encryptedBytes and encryptedBytes2 should be identical, so they should result in the same decrypted text - but they don't: 
Debug.WriteLine(testStringDecrypted == "test string"); //Result: True
Debug.WriteLine(testStringDecrypted2 == "test string"); //Result: FALSE
//testStringDecrypted2 is now "૱﷜ୱᵪ㭈盐æing". Curiously, the last three letters are the same.
//WTF?

所以问题是重用。在深入讨论如何重置IV之前,这有多重要? - H H
你没有提供第一个问题的任何代码,因此无法回答。 - H H
抱歉 - 我不太明白你的意思。代码示例是一个更大的类中问题的简化,该类提供了简单的字符串加密和解密方法。问题出现的原因是我正在使用BinaryFormatter将一些加密文本序列化到数据库中,并在以后重新加载它,因此我的加密文本调用的序列化结果在解密后并尝试使用时与原始文本不同。希望这样能澄清事情。 - wwarby
但是你只发布了解决方法,没有发布不起作用的代码部分。请尝试发布一个完整的代码片段来演示问题。此外,请每个问题发布1个问题。 - H H
Henk,抱歉 - 我没有跟上你的思路,因为我不明白代码为什么不起作用。我甚至没有意识到我已经重复使用了解密器,这就是代码不起作用的原因。无论如何,我得到了答案 - 感谢你的帮助。 - wwarby
3个回答

6

这似乎是.NET框架AES实现中的一个bug,与你引用的ICryptoTransform有关:

provider.CreateDecryptor(keyBytes, ivBytes);

CanReuseTransform返回true,但是似乎它在解密后没有清空输入缓冲区。有几种解决方法可以使其正常工作。

选项1 创建第二个解密器并用它来解密第二个字符串。

var key = new Rfc2898DeriveBytes("test password", Encoding.Unicode.GetBytes("test salt"));
var provider = new AesCryptoServiceProvider { Padding = PaddingMode.PKCS7, KeySize = 256 };
var keyBytes = key.GetBytes(provider.KeySize >> 3);
var ivBytes = key.GetBytes(provider.BlockSize >> 3);
var encryptor = provider.CreateEncryptor(keyBytes, ivBytes);
var decryptor = provider.CreateDecryptor(keyBytes, ivBytes);
var decryptor2 = provider.CreateDecryptor(keyBytes, ivBytes);

var testStringBytes = Encoding.Unicode.GetBytes("test string");
var testStringEncrypted = Convert.ToBase64String(encryptor.TransformFinalBlock(testStringBytes, 0, testStringBytes.Length));

//Prove that the encryption has resulted in the following string
Console.WriteLine(testStringEncrypted == "cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc="); //Result: True

//Decrypt the encrypted text from a hardcoded string literal
var encryptedBytes = Convert.FromBase64String("cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc=");

var testStringDecrypted = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length));

//Decrypt the encrypted text from the string result of the encryption process
var encryptedBytes2 = Convert.FromBase64String(testStringEncrypted);

var testStringDecrypted2 = Encoding.Unicode.GetString(decryptor2.TransformFinalBlock(encryptedBytes2, 0, encryptedBytes2.Length));

//encryptedBytes and encryptedBytes2 should be identical, so they should result in the same decrypted text - but they don't: 
Console.WriteLine(testStringDecrypted == "test string"); //Result: True
Console.WriteLine(testStringDecrypted2 == "test string"); //Result: True

Console.Read();

选项2 使用RijandaelManaged(或AesManaged)代替AesCryptoServiceProvider,它应该是相同的算法(虽然AesCryptoServiceProvider和AesManaged都将块大小限制为128)

var key = new Rfc2898DeriveBytes("test password", Encoding.Unicode.GetBytes("test salt"));
var provider = new RijndaelManaged { Padding = PaddingMode.PKCS7, KeySize = 256 };
var keyBytes = key.GetBytes(provider.KeySize >> 3);
var ivBytes = key.GetBytes(provider.BlockSize >> 3);
var encryptor = provider.CreateEncryptor(keyBytes, ivBytes);
var decryptor = provider.CreateDecryptor(keyBytes, ivBytes);

var testStringBytes = Encoding.Unicode.GetBytes("test string");
var testStringEncrypted = Convert.ToBase64String(encryptor.TransformFinalBlock(testStringBytes, 0, testStringBytes.Length));

//Prove that the encryption has resulted in the following string
Console.WriteLine(testStringEncrypted == "cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc="); //Result: True

//Decrypt the encrypted text from a hardcoded string literal
var encryptedBytes = Convert.FromBase64String("cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc=");

var testStringDecrypted = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length));

//Decrypt the encrypted text from the string result of the encryption process
var encryptedBytes2 = Convert.FromBase64String(testStringEncrypted);

var testStringDecrypted2 = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes2, 0, encryptedBytes2.Length));

//encryptedBytes and encryptedBytes2 should be identical, so they should result in the same decrypted text - but they don't: 
Console.WriteLine(testStringDecrypted == "test string"); //Result: True
Console.WriteLine(testStringDecrypted2 == "test string"); //Result: True

Console.Read();

选项 3: 改用 using 语句

var key = new Rfc2898DeriveBytes("test password", Encoding.Unicode.GetBytes("test salt"));
var provider = new AesCryptoServiceProvider { Padding = PaddingMode.PKCS7, KeySize = 256 };
var keyBytes = key.GetBytes(provider.KeySize >> 3);
var ivBytes = key.GetBytes(provider.BlockSize >> 3);
var encryptor = provider.CreateEncryptor(keyBytes, ivBytes);

var testStringBytes = Encoding.Unicode.GetBytes("test string");
var testStringEncrypted = Convert.ToBase64String(encryptor.TransformFinalBlock(testStringBytes, 0, testStringBytes.Length));

//Prove that the encryption has resulted in the following string
Console.WriteLine(testStringEncrypted == "cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc="); //Result: True

//Decrypt the encrypted text from a hardcoded string literal
var encryptedBytes = Convert.FromBase64String("cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc=");

string testStringDecrypted, testStringDecrypted2;

using (var decryptor = provider.CreateDecryptor(keyBytes, ivBytes))
{
    testStringDecrypted =
        Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length));
}

//Decrypt the encrypted text from the string result of the encryption process
var encryptedBytes2 = Convert.FromBase64String(testStringEncrypted);

using (var decryptor = provider.CreateDecryptor(keyBytes, ivBytes))
{
    testStringDecrypted2 =
        Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes2, 0, encryptedBytes2.Length));
}

//encryptedBytes and encryptedBytes2 should be identical, so they should result in the same decrypted text - but they don't: 
Console.WriteLine(testStringDecrypted == "test string"); //Result: True
Console.WriteLine(testStringDecrypted2 == "test string"); //Result: True

Console.Read();

1
非常感谢 - 这个答案(以及之后的另外两个)正是我需要的。我没有意识到一旦我创建了解密器,我只能使用它一次。看来我陷入了编写过于高效代码的陷阱 - 在我简化这个例子的真实类中,解密器(和加密器)被存储为类的静态字段,以便我不会不断地重新创建它们。显然这不是一个好主意。我现在已经重构了这个类,一切都运行良好。有时我讨厌密码学 - 它太挑剔了! - wwarby
我已经使用了 using,尝试了 AesManaged - 仍然存在问题。https://dev59.com/c27Xa4cB1Zd3GeqPrppS - LCJ
请注意,RijandaelManaged不符合FIPS标准。因此,如果您希望您的应用程序符合FIPS标准,您应该选择选项1。 - Akshay

2
尽管在两种情况下使用的输入相同,但问题在于 decryptor.TransformFinalBlock() 的行为在第一次调用后会发生变化。无论值是在字符串字面量还是变量中都没有区别。这个页面似乎表明,在第一次使用后,decryptor 会“重置”自己到某个初始状态:

http://www.pcreview.co.uk/forums/icryptotransform-transformfinalblock-behavior-bug-t1233029.html

似乎您可以通过重新调用provider.CreateDecryptor(keyBytes, ivBytes)为每个想要进行的解密获取一个新的解密器来解决这个问题:
        //Decrypt the encrypted text from a hardcoded string literal
        var encryptedBytes = Convert.FromBase64String("cc1zurZinx4yxeSB0XDzVziEUNJlFXsLzD2p9TWnxEc=");
        var testStringDecrypted = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length));

        decryptor = provider.CreateDecryptor(keyBytes, ivBytes);

        //Decrypt the encrypted text from the string result of the encryption process
        var encryptedBytes2 = Convert.FromBase64String(testStringEncrypted);
        var testStringDecrypted2 = Encoding.Unicode.GetString(decryptor.TransformFinalBlock(encryptedBytes2, 0, encryptedBytes2.Length));

感谢您的回答!我得到了三个答案,它们都大体正确,所以我选择了第一个回答并在其他答案上按了“up” - 希望我的堆栈溢出礼仪是正确的(我对此还比较新)。 - wwarby

1

根据评论中提到的,我会认为这是重复使用解密器的问题,可能仍然在其状态中保留了第一次解密的最后一个块,因此它没有从头开始,导致你得到了奇怪的结果。

实际上,我前几天不得不编写一个AES字符串加密器/解密器,我在这里包含了它以及单元测试(需要Xunit)。

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Xunit;

public interface IStringEncryptor {
    string EncryptString(string plainText);
    string DecryptString(string encryptedText);
}

public class AESStringEncryptor : IStringEncryptor {
    private readonly Encoding _encoding;
    private readonly byte[] _key;
    private readonly Rfc2898DeriveBytes _passwordDeriveBytes;
    private readonly byte[] _salt;

    /// <summary>
    /// Overload of full constructor that uses UTF8Encoding as the default encoding.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="salt"></param>
    public AESStringEncryptor(string key, string salt)
        : this(key, salt, new UTF8Encoding()) {
    }

    public AESStringEncryptor(string key, string salt, Encoding encoding) {
        _encoding = encoding;
        _passwordDeriveBytes = new Rfc2898DeriveBytes(key, _encoding.GetBytes(salt));
        _key = _passwordDeriveBytes.GetBytes(32);
        _salt = _passwordDeriveBytes.GetBytes(16);
    }

    /// <summary>
    /// Encrypts any string to a Base64 string
    /// </summary>
    /// <param name="plainText"></param>
    /// <exception cref="ArgumentNullException">String to encrypt cannot be null or empty.</exception>
    /// <returns>A Base64 string representing the encrypted version of the plainText</returns>
    public string EncryptString(string plainText) {
        if (string.IsNullOrEmpty(plainText)) {
            throw new ArgumentNullException("plainText");
        }

        using (var alg = new RijndaelManaged { BlockSize = 128, FeedbackSize = 128, Key = _key, IV = _salt })
        using (var ms = new MemoryStream())
        using (var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write)) {
            var plainTextBytes = _encoding.GetBytes(plainText);

            cs.Write(plainTextBytes, 0, plainTextBytes.Length);
            cs.FlushFinalBlock();

            return Convert.ToBase64String(ms.ToArray());
        }
    }

    /// <summary>
    /// Decrypts a Base64 string to the original plainText in the given Encoding
    /// </summary>
    /// <param name="encryptedText">A Base64 string representing the encrypted version of the plainText</param>
    /// <exception cref="ArgumentNullException">String to decrypt cannot be null or empty.</exception>
    /// <exception cref="CryptographicException">Thrown if password, salt, or encoding is different from original encryption.</exception>
    /// <returns>A string encoded</returns>
    public string DecryptString(string encryptedText) {
        if (string.IsNullOrEmpty(encryptedText)) {
            throw new ArgumentNullException("encryptedText");
        }

        using (var alg = new RijndaelManaged { BlockSize = 128, FeedbackSize = 128, Key = _key, IV = _salt })
        using (var ms = new MemoryStream())
        using (var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write)) {
            var encryptedTextBytes = Convert.FromBase64String(encryptedText);

            cs.Write(encryptedTextBytes, 0, encryptedTextBytes.Length);
            cs.FlushFinalBlock();

            return _encoding.GetString(ms.ToArray());
        }
    }
}

public class AESStringEncryptorTest {
    private const string Password = "TestPassword";
    private const string Salt = "TestSalt";

    private const string Plaintext = "This is a test";

    [Fact]
    public void EncryptionAndDecryptionWorkCorrectly() {
        var aesStringEncryptor = new AESStringEncryptor(Password, Salt);

        string encryptedText = aesStringEncryptor.EncryptString(Plaintext);

        Assert.NotEqual(Plaintext, encryptedText);

        var aesStringDecryptor = new AESStringEncryptor(Password, Salt);

        string decryptedText = aesStringDecryptor.DecryptString(encryptedText);

        Assert.Equal(Plaintext, decryptedText);
    }

    [Fact]
    public void EncodingsWorkWhenSame()
    {
        var aesStringEncryptor = new AESStringEncryptor(Password, Salt, Encoding.ASCII);

        string encryptedText = aesStringEncryptor.EncryptString(Plaintext);

        Assert.NotEqual(Plaintext, encryptedText);

        var aesStringDecryptor = new AESStringEncryptor(Password, Salt, Encoding.ASCII);

        string decryptedText = aesStringDecryptor.DecryptString(encryptedText);

        Assert.Equal(Plaintext, decryptedText);
    }

    [Fact]
    public void EncodingsFailWhenDifferent() {
        var aesStringEncryptor = new AESStringEncryptor(Password, Salt, Encoding.UTF32);

        string encryptedText = aesStringEncryptor.EncryptString(Plaintext);

        Assert.NotEqual(Plaintext, encryptedText);

        var aesStringDecryptor = new AESStringEncryptor(Password, Salt, Encoding.UTF8);

        Assert.Throws<CryptographicException>(() => aesStringDecryptor.DecryptString(encryptedText));
    }

    [Fact]
    public void EncryptionAndDecryptionWithWrongPasswordFails()
    {
        var aes = new AESStringEncryptor(Password, Salt);

        string encryptedText = aes.EncryptString(Plaintext);

        Assert.NotEqual(Plaintext, encryptedText);

        var badAes = new AESStringEncryptor(Password.ToLowerInvariant(), Salt);

        Assert.Throws<CryptographicException>(() => badAes.DecryptString(encryptedText));
    }

    [Fact]
    public void EncryptionAndDecryptionWithWrongSaltFails()
    {
        var aes = new AESStringEncryptor(Password, Salt);

        string encryptedText = aes.EncryptString(Plaintext);

        Assert.NotEqual(Plaintext, encryptedText);

        var badAes = new AESStringEncryptor(Password, Salt.ToLowerInvariant());

        Assert.Throws<CryptographicException>(() => badAes.DecryptString(encryptedText));
    }
}

感谢您的回答!我收到了三个答案,都大体上是正确的,所以我选择了第一个回答作为正确答案 - 希望我的堆栈溢出礼仪做得对(我还是比较新手)。 - wwarby

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接