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
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.