Search code examples
c#hashaespbkdf2hmacsha1

C# - PBDKF2, null values


I'm implementing AES in C# and for the IV and key I am using PBDKF2 through the Rfc2898DeriveBytes Class. When I run it, the input text is encrypted, when I view the data in the "key" variable, there's an error that says "Hash = 'key.m_hmacsha1.Hash' threw an exception of type 'System.NullReferenceException'", along with other null values/0s in other methods. I suspect I am not implementing it correctly in my code, and I am having trouble trying to diagnose it. I hope to gain insight from people who have implemented AES in C#. Also, if a user enters in the wrong key, how would I catch this in an exception? The code is posted below.

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

namespace AES
{
class Program
{
    public static byte[] salt;
    public static byte[] saltBytes;

    static void Main(string[] args)
    {
        var encrypted = secure.EncryptText("abc", "123");

        Console.WriteLine(encrypted);

        Console.WriteLine(secure.DecryptText(encrypted, "123"));
    }

    public string EncryptText(string input, string password)
    {
        // Get the bytes of the string
        byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
        byte[] passwordBytes = Encoding.UTF8.GetBytes(password);

        // Hash the password with SHA256
        passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

        byte[] bytesEncrypted = AES_Encrypt(bytesToBeEncrypted, passwordBytes);

        string result = Convert.ToBase64String(bytesEncrypted);

        return result;
    }

    public string DecryptText(string input, string password)
    {
        // Get the bytes of the string
        byte[] bytesToBeDecrypted = Convert.FromBase64String(input);
        byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
        passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

        byte[] bytesDecrypted = AES_Decrypt(bytesToBeDecrypted, passwordBytes);

        string result = Encoding.UTF8.GetString(bytesDecrypted);

        return result;
    }

    public byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
    {
        byte[] encryptedBytes = null;

        // Set your salt here, change it to meet your flavor:
        // The salt bytes must be at least 8 bytes.

        new RNGCryptoServiceProvider().GetBytes(salt = new byte[32]);
        saltBytes = salt;

        using (MemoryStream ms = new MemoryStream())
        {
            using (RijndaelManaged AES = new RijndaelManaged())
            {

                AES.KeySize = 256;
                AES.BlockSize = 128;

                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                var iv = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                AES.IV = iv.GetBytes(AES.BlockSize / 8);

                AES.Mode = CipherMode.CBC;

                using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                    cs.Close();
                }
                encryptedBytes = ms.ToArray();
            }
        }

        return encryptedBytes;
    }

    public byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
    {

        byte[] decryptedBytes = null;

        // Set your salt here, change it to meet your flavor:
        // The salt bytes must be at least 8 bytes.

        using (MemoryStream ms = new MemoryStream())
        {
            using (RijndaelManaged AES = new RijndaelManaged())
            {
                AES.KeySize = 256;
                AES.BlockSize = 128;

                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                var iv = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                AES.IV = iv.GetBytes(AES.BlockSize / 8);

                AES.Mode = CipherMode.CBC;

                using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                    cs.Close();
                }
                decryptedBytes = ms.ToArray();
            }
        }

        return decryptedBytes;
    }

}
}

Solution

  • when I view the data in the "key" variable, there's an error that says "Hash = 'key.m_hmacsha1.Hash' threw an exception of type 'System.NullReferenceException'", along with other null values/0s in other methods.

    You're inspecting private fields of a class you don't own. That doesn't always lead to sensible results. Instead, you should rely only on the public API.

    if a user enters in the wrong key, how would I catch this in an exception?

    The odds are likely (~15/16) that you'll get an exception indicating "the padding is invalid and cannot be removed". But really you should do this on top of encryption either by

    a) computing an HMAC (e.g. HMACSHA256) over the ciphertext, then on decryption verify the HMAC (failure to validate indicates the wrong password or that the data has been tampered with), or

    b) Having a well-formed piece of data inside the plaintext that can be used for validation.

    (A) is the better answer for various reasons.


    You have code like

    var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
    AES.Key = key.GetBytes(AES.KeySize / 8);
    var iv = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
    AES.IV = iv.GetBytes(AES.BlockSize / 8);
    

    Since your password, salt, and iteration count are the same for both key and iv, your actual IV is the first 128 bits of your encryption key. If you want it to be a computed value you'd want to vary the salt (e.g. set the last byte of the salt to 0 for the encryption key, 1 for the IV, and 2 for the HMAC key), or do it as the next output bytes (ask for 32 + 16 + 32 bytes and chop it up accordingly... or, given the .NET implementation, call GetBytes(32), GetBytes(16), GetBytes(32) (Rfc2898DeriveBytes.GetBytes is a streaming API)).

    Or, for the IV, just use the randomly generated one during encryption and transmit it with the ciphertext. (If you do that, be sure to include it in your HMAC computation)


    You store salt and saltBytes in fields (though you set one to the other, so it's unclear why you have two fields). You need to transmit the salt value along with the ciphertext and have the decryptor accept it as input.

    Essentially, your functions should be Encrypt(passphrase, data) => <salt, hmac, ciphertext> (or <salt, iv, hmac, ciphertext>) and Decrypt(passphrase, salt, [iv,] hmac, ciphertext) => data. (Depending on if the IV is transmitted or derived)