问题
I'm having trouble with encryption and decryption of text in C# (VS2012, .NET 4.5). Specifically, When I encrypt and subsequently decrypt a string, the output is not the same as the input. However, bizarrely, if I copy the encrypted output and hardcode it as a string literal, decryption works. The following code sample illustrates the problem. What am I doing wrong?
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?
回答1:
This seems to be a bug in the .NET framework implementation of AES the ICryptoTransform that you're referencing with your line:
provider.CreateDecryptor(keyBytes, ivBytes);
returns true for CanReuseTransform however it seems to not be clearing the input buffer after decryption. There are a few solutions to get this to work.
Option 1 Create a second decryptor and decrypt the second string with that.
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();
Option 2 Use RijandaelManaged (or AesManaged) instead of the AesCryptoServiceProvider, it should be the same algorithm (although both AesCryptoServiceProvider and AesManaged restrict the block size to 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();
Option 3: Use a using statement instead
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();
回答2:
Even though you are using the same inputs in both cases, the issue is that the behavior of decryptor.TransformFinalBlock() changes after the first time it is called. It makes no difference whether the values are in string literals or variables. This page seems to suggest that the decryptor is "resetting" itself to some initial state after the first use:
http://www.pcreview.co.uk/forums/icryptotransform-transformfinalblock-behavior-bug-t1233029.html
It seems that you can get around this by re-calling provider.CreateDecryptor(keyBytes, ivBytes)
to get a new decryptor for each decryption you want to do:
//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));
回答3:
I would assume, as mentioned in the comments, that it's an issue of reusing the decryptor, which probably still has the last block from the first decryption somewhere in its state, so it's not starting from scratch, and you're getting weird results.
I actually had to write an AES string encryptor/decryptor the other day, which I've included here, along with the unit tests (requires 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));
}
}
来源:https://stackoverflow.com/questions/14175272/why-is-this-string-not-equal-when-it-is-decrypted-a-second-time-with-aescryptose