3

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?
Henk Holterman
  • 236,989
  • 28
  • 287
  • 464
wwarby
  • 1,415
  • 1
  • 15
  • 29
  • So the issue is re-use. Before diving into a discussion about how to reset the IV: how important is this? – Henk Holterman Jan 05 '13 at 19:19
  • You don't provide any code for the first problem, not answerable. – Henk Holterman Jan 05 '13 at 19:20
  • Apologies - I don't quite understand what you're saying. The code sample is a simplification of a problem in a much larger class that provides simple Encrypt and Decrypt methods for strings. The reason the problem presents itself is because I am using a BinaryFormatter to serialize some encrypted text into the a database and load it back again later, and thus the serialized result of my call to encrypt text does not result in the same text when I decrypt it and try to use it. Hope that clarifies things. – wwarby Jan 05 '13 at 19:34
  • But you only posted the work-around, nothing from the non-functioning code part. Try to post a complete piece of code that does demonstrate the problem. Also, post 1 question per question. – Henk Holterman Jan 05 '13 at 19:47
  • Henk, apologies - I wasn't following you because I didn't understand why the code didn't work. It didn't even register in my consciousness that I had reused the decryptor and that that is why the code wasn't working. Anyway, I got my answer - thanks for your input. – wwarby Jan 05 '13 at 23:52

3 Answers3

6

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();
nerdybeardo
  • 4,535
  • 19
  • 29
  • 1
    Thank you so much - this (and the other two that came after) was the answer I needed. It hadn't occurred to me that once I created a decryptor I could only use it once. It seems that here I fell into the trap of trying to write overly efficient code - in the real class which I simplified this example from, the decryptor (and encryptor) are stored as static fields on a class so that I don't keep recreating them. Clearly this was not a good idea. I've refactored the class now and everything works fine. I hate cryptography sometimes - it's so finicky! – wwarby Jan 05 '13 at 23:47
  • I have used `using`; tried AesManaged - still an issue http://stackoverflow.com/questions/14937707/getting-incorrect-decryption-value-using-aescryptoserviceprovider – LCJ Feb 18 '13 at 14:30
  • Note: But please note that RijandaelManaged is not FIPS compliance. So if you want your application to be FIPS compliance, you should go with Option 1. – Akshay Aug 21 '14 at 06:05
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));
JLRishe
  • 90,548
  • 14
  • 117
  • 150
  • Thanks for your input! I had three answers, all of which were broadly correct, so I've ticked the first one that came in as the right one and pressed "up" on the others - hope I've got my stack overflow etiquette right there (I'm fairly new to it). – wwarby Jan 05 '13 at 23:50
1

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));
    }
}
Chris Doggett
  • 17,776
  • 4
  • 55
  • 84
  • Thanks for your input! I had three answers, all of which were broadly correct, so I've ticked the first one that came in as the right one - hope I've got my stack overflow etiquette right there (I'm fairly new to it). – wwarby Jan 05 '13 at 23:51