Search code examples
c#.netencryptionaes.net-7.0

".NET 7 Way" of File Encryption? RijndaelManaged is obsolete


I am trying to write my own Encrypt/Decrypt methods in a .NET 7 solution (NOT 4.8 or even 6). I know that RijndaelManaged has been the go-to implementation in the past, but Microsoft has now marked that as Obsolete so I want to write it "the .NET 7 Way".

While my methods seem to run to completion most of the time, I occasionally get exceptions. Worse, none of the files I run my methods on, whether text or binary files (images, executables, etc) get restored back to their original bits properly. All wind up being corrupted in some fashion.

I would be super grateful if someone were to point out what I might have missed (or gotten just plain wrong) in my code.

Here is my Encrypt method:

public static void EncryptFile(string sourcePath, string destinationPath, string password)
{
    if (string.IsNullOrWhiteSpace(password)) return;

    using FileStream outputStream = File.OpenWrite(destinationPath);

    byte[] salt = RandomNumberGenerator.GetBytes(32);
    outputStream.Write(salt, 0, salt.Length);
    Aes aes = GetAES(password, salt);

    using CryptoStream cryptoStream = new CryptoStream(outputStream, aes.CreateEncryptor(), CryptoStreamMode.Write);
    using FileStream inputStream = new FileStream(sourcePath, FileMode.Open);

    int readPosition;
    byte[] buffer = new byte[1024 * 1024];
    try
    {
        while ((readPosition = inputStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            cryptoStream.Write(buffer, 0, readPosition);
        }

        inputStream.Close();
    }
    catch (Exception ex)
    {

    }
    finally
    {
        cryptoStream.Close();
        outputStream.Close();
    }
}

And here is my Decrypt method:

public static void DecryptFile(string sourcePath, string destinationPath, string password)
{
    if (string.IsNullOrWhiteSpace(password)) return;

    using FileStream inputStream = new FileStream(sourcePath, FileMode.Open);

    byte[] salt = new byte[32];
    inputStream.Read(salt, 0, salt.Length);
    Aes aes = GetAES(password, salt);

    using CryptoStream cryptoStream = new CryptoStream(inputStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
    using FileStream outputStream = new FileStream(destinationPath, FileMode.Create);

    int readPosition;
    byte[] buffer = new byte[1024 * 1024];
    try
    {
        while ((readPosition = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            outputStream.Write(buffer, 0, readPosition);
        }

        cryptoStream.Close();
    }
    catch(Exception ex)
    {
    }
    finally
    {
        outputStream.Close();
        inputStream.Close();
    }
}

I refactored out the Aes object which is common to both, into a separate initialization function:

private static Aes GetAES(string password, byte[] salt)
{
    byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
    Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(passwordBytes, salt, 50000, HashAlgorithmName.SHA256);

    using Aes aes = Aes.Create();
    aes.KeySize = 256;
    aes.BlockSize = 128;
    aes.Padding = PaddingMode.Zeros;
    aes.Mode = CipherMode.CBC;
    aes.Key = key.GetBytes(aes.KeySize / 8);
    aes.IV = key.GetBytes(aes.BlockSize / 8);

    return aes;
}

I've experimented with the PaddingMode, most notably using Zeros and PKCS7, but have not gotten it to work for me.

What am I missing and/or what should I do to make this work correctly? I need to be able to encrypt/decrypt any size and/or type of file (in Windows, at least), and to have it work in .NET 7 and beyond.

Thanks in advance for your time.

enter code here

Solution

  • Since you've chosen to use method GetAES to create a new instance of Aes, it can't be disposed of within GetAES which means a using declaration shouldn't be used.

    GetAES:

    private static Aes GetAES(string password, byte[] salt)
    {
        byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
        Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(passwordBytes, salt, 50000, HashAlgorithmName.SHA256);
    
        //although 'Aes' implements IDisposable, it can't be disposed of in this method
        //because a reference is being created in the calling method
        //using Aes aes = Aes.Create(); 
        Aes aes = Aes.Create(); 
    
        aes.KeySize = 256;
        aes.BlockSize = 128;
        //aes.Padding = PaddingMode.Zeros; 
        aes.Padding = PaddingMode.PKCS7;
        aes.Mode = CipherMode.CBC;
        aes.Key = key.GetBytes(aes.KeySize / 8);
        aes.IV = key.GetBytes(aes.BlockSize / 8);
    
        return aes;
    }
    

    In method EncryptFile, a using declaration was added using Aes aes = GetAES(password, salt); and readPosition was renamed to bytesRead because the name seemed a bit deceiving - rename it if you want to. I resized the buffer array - it can be changed to the desired size. A call to CryptoStream FlushFinalBlock method was added. Added code to keep track of total bytes read, as well as, the number of read iterations.

    EncryptFile:

    public static void EncryptFile(string sourcePath, string destinationPath, string password)
    {
        if (string.IsNullOrWhiteSpace(password)) return;
    
        using FileStream outputStream = File.OpenWrite(destinationPath);
    
        byte[] salt = RandomNumberGenerator.GetBytes(32);
        outputStream.Write(salt, 0, salt.Length);
    
        using Aes aes = GetAES(password, salt);
    
        using CryptoStream cryptoStream = new CryptoStream(outputStream, aes.CreateEncryptor(), CryptoStreamMode.Write);
        //using ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
        //using CryptoStream cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write);
        using FileStream inputStream = new FileStream(sourcePath, FileMode.Open);
    
        long totalBytesRead = 0;
        int bytesRead = 0;
        //byte[] buffer = new byte[1024 * 1024];
        byte[] buffer = new byte[1024 * 4]; //4096
    
        try
        {
            long i = 0;
            while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                //add
                totalBytesRead += bytesRead;
                Debug.Write($"[{i}] bytesRead: {bytesRead} totalBytesRead: {totalBytesRead}{Environment.NewLine}");
    
                cryptoStream.Write(buffer, 0, bytesRead);
    
                //increment
                i++;
            }
    
            inputStream.Close();
    
            //flush
            cryptoStream.FlushFinalBlock();
        }
        catch (Exception ex)
        {
            throw;
        }
        finally
        {
            cryptoStream.Close();
            outputStream.Close();
        }
    }
    
    

    The changes in method DecryptFile are almost identical to those in EncryptFile. A using declaration was added using Aes aes = GetAES(password, salt); and readPosition was renamed to bytesRead - rename it if you want to. I resized the buffer - it can be changed to the desired size. A call to Stream Flush method was added. Added code to keep track of total bytes read, as well as, the number of read iterations.

    DecryptFile:

    public static void DecryptFile(string sourcePath, string destinationPath, string password)
    {
        if (string.IsNullOrWhiteSpace(password)) return;
    
        using FileStream inputStream = new FileStream(sourcePath, FileMode.Open);
    
        byte[] salt = new byte[32];
        inputStream.Read(salt, 0, salt.Length);
    
        //'Aes' implements IDisposable
        using Aes aes = GetAES(password, salt);
    
        using CryptoStream cryptoStream = new CryptoStream(inputStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
        //using ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
        //using CryptoStream cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read);
        using FileStream outputStream = new FileStream(destinationPath, FileMode.Create);
    
        try
        {
            long totalBytesRead = 0;
            int bytesRead = 0;
            //byte[] buffer = new byte[1024 * 1024];
            byte[] buffer = new byte[1024 * 4]; //4096
    
            long i = 0;
            while ((bytesRead = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                //add
                totalBytesRead += bytesRead;
                Debug.Write($"[{i}] bytesRead: {bytesRead} totalBytesRead: {totalBytesRead}{Environment.NewLine}");
    
                outputStream.Write(buffer, 0, bytesRead);
    
                //increment
                i++;
            }
    
            //flush
            outputStream.Flush();
    
            //cryptoStream.Close(); //close in finally instead
        }
        catch (System.Security.Cryptography.CryptographicException ex)
        {
            if (ex.Message == "Padding is invalid and cannot be removed.")
            {
                //when an invalid password is entered and Padding = PaddingMode.PKCS7
                //one receives this exception
                Debug.WriteLine($"Error: Invalid password.");
                throw new Exception("Invalid password.", ex);
            }
            else
            {
                Debug.WriteLine($"Error: {ex.Message}");
                throw;
            }
        }
        catch (Exception ex)
        {
            throw;
        }
        finally
        {
            cryptoStream.Close();
            outputStream.Close();
            inputStream.Close();
        }
    }
    

    Additional Resources:


    Here's some additional information about KeySize and BlockSize.

    What are valid values for KeySize and BlockSize?

    If one looks at the documentation for Aes, one sees:

    public abstract class Aes : System.Security.Cryptography.SymmetricAlgorithm
    

    If one looks to the left-side of the window one can expand both Properties and Methods. However, MS doesn't show inherited Properties and Methods as part of the documentation for the class that inherits them. Therefore, it's necessary to review the documentation for each inherited class. In this case, we'll need to look at the documentation for System.Security.Cryptography.SymmetricAlgorithm which is where we'll find the following properties:

    In both LegalBlockSizes and LegalKeySizes documentation one will find examples of how to obtain legal block sizes and legal key sizes. Also see KeySizes.

    Alternatively, one can use the code below:

    private string GetLegalBlockSizes()
    {
        string output = string.Empty;
    
        using (Aes aes = Aes.Create())
        {
            foreach (KeySizes ks in aes.LegalBlockSizes)
            {
               //append
                output += $"LegalBlockSizes MinSize: {ks.MinSize} MaxSize: {ks.MaxSize} SkipSize: {ks.SkipSize}{Environment.NewLine}";
    
                for (int i = ks.MinSize; i <= ks.MaxSize;)
                {
                    //1 byte / 8 bits
                    //append
                    output += $"    {i} bits  =  {i / 8} bytes{Environment.NewLine}";
    
                    if (ks.SkipSize == 0)
                        break;
    
                    //increment
                    i += ks.SkipSize;
                }
            }
        }
    
        return output;
    }
    
    private string GetLegalKeySizes()
    {
        string output = string.Empty;
    
        using (Aes aes = Aes.Create())
        {
            //get legal key sizes
            foreach (KeySizes ks in aes.LegalKeySizes)
            {
                //append
                output += $"LegalKeySizes MinSize: {ks.MinSize} MaxSize: {ks.MaxSize} SkipSize: {ks.SkipSize}{Environment.NewLine}";
    
                for (int i = ks.MinSize; i <= ks.MaxSize;)
                {
                    //1 byte / 8 bits
                    //append
                    output += $"    {i} bits  =  {i / 8} bytes {Environment.NewLine}";
    
                    if (ks.SkipSize == 0)
                        break;
    
                    //increment
                    i += ks.SkipSize;
                }
            }
        }
    
        return output;
    }
    

    Running the code above results in the following output:

    LegalBlockSizes MinSize: 128 MaxSize: 128 SkipSize: 0
        128 bits  =  16 bytes
    
    LegalKeySizes MinSize: 128 MaxSize: 256 SkipSize: 64
        128 bits  =  16 bytes 
        192 bits  =  24 bytes 
        256 bits  =  32 bytes 
    

    Also see Rfc2898DeriveBytes.

    If interested, the source code for Aes and Rfc2898DeriveBytes is publicly available.