I wrote some AES encryption/decryption methods with the following requirements:
- Inputs should be easy-to-use strings.
- Something encrypted in a .NET 6 app using these methods should be able to be decrypted in a .NET Framework 4.8 app using the same methods.
- I do NOT need military or banking grade encryption.
- I do NOT desire too many choices regarding
CipherMode,PaddingMode, etc. - Extra: rather than decrypt to just a
string, I can also decrypt to aSecureString, which can be passed to aNetworkCredentialconstructor.
That said, I do want to emphasize that I am very much a novice regarding cryptography. Thus my need to make something simple for me to use. Also: the code shown works perfectly well for me in both .NET 6 and .NET Framework 4.8.
SimpleAes6 Class
using System;
using System.Linq;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;
namespace Davin.Cryptography
{
// The intent is not for military or banking grade cryptography, but something strong enough
// to pass an internal company audit. At the very least, it is far better using this than
// storing plain text passwords in Config files, since that would immediately raise an audit flag.
#pragma warning disable IDE0063 // Use simple 'using' statement
#pragma warning disable IDE0090 // Use 'new(...)'
public static class SimpleAes6
{
// The public outer or wrapper class "SimpleAes6" works with string inputs and outputs.
// The private inner class "AesHelper" works with byte arrays.
// The suffix 6 was used since this was written and tested specifically for .NET 6.
// It has been confirmed to work with .NET Framework 4.8, that is to say that
// cipherText encrypted from a .NET 6 application can be decrypted back to plainText
// by a .NET Framework application using the same code base contained here.
/// <summary>
/// Simple AES encryption of a plain text string using the specified secret Key phrase and IV Salt.
/// The same Key phrase and IV Salt must be used later when you decrypt the cipher text back to plain text.
/// </summary>
/// <param name="plainText">The plain text you wish to encrypt to cipher text.</param>
/// <param name="key">The secret key. Minimum length = 8, Maximum Length = 128.</param>
/// <param name="ivSalt">A secret salt for the initialization vector. Minimum length = 8, Maximum Length = 128.</param>
/// <returns>An encrypted cipher text string suitable for passwords to be stored safely into XML or JSON files.</returns>
public static string Encrypt(string plainText, string key, string ivSalt)
{
ValidateString(plainText, nameof(plainText), minLength: 1);
ValidateString(key, nameof(key), minLength: 8, maxLength: 128);
ValidateString(ivSalt, nameof(ivSalt), minLength: 8, maxLength: 128);
// Get the bytes of the respective strings
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] ivBytes = Encoding.UTF8.GetBytes(ivSalt);
// Hash the Key with SHA256
keyBytes = SHA256.Create().ComputeHash(keyBytes);
byte[] bytesEncrypted = AesHelper.Encrypt(plainBytes, keyBytes, ivBytes);
return Convert.ToBase64String(bytesEncrypted);
}
/// <summary>
/// Decrypts the cipher text back to plain text using the same secret Key phrase and IV Salt that was
/// previously used to encrypt.
/// </summary>
/// <param name="cipherText">A string encryped earlier using the same secret Key phrase and IV Salt.</param>
/// <param name="key">The secret Key. Minimum length = 8, Maximum Length = 128.</param>
/// <param name="ivSalt">A secret salt for the initialization vector. Minimum length = 8, Maximum Length = 128.</param>
/// <returns>A plain text string.</returns>
public static string DecryptToString(string cipherText, string key, string ivSalt)
{
ValidateString(cipherText, nameof(cipherText), minLength: 1);
ValidateString(key, nameof(key), minLength: 8, maxLength: 128);
ValidateString(ivSalt, nameof(ivSalt), minLength: 8, maxLength: 128);
// Get the bytes of the string
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] ivBytes = Encoding.UTF8.GetBytes(ivSalt);
keyBytes = SHA256.Create().ComputeHash(keyBytes);
byte[] bytesDecrypted = AesHelper.Decrypt(cipherBytes, keyBytes, ivBytes);
return Encoding.UTF8.GetString(bytesDecrypted);
}
/// <summary>
/// Decrypts the cipher text back to a SecureString using the same secret pass phrase and salt that was
/// originally used to encryt the text.
/// </summary>
/// <param name="cipherText">A string encryped earlier using the same secret Key phrase and IV Salt.</param>
/// <param name="key">The secret Key. Minimum length = 8, Maximum Length = 128.</param>
/// <param name="ivsalt">A secret salt for the initialization vector. Minimum length = 8, Maximum Length = 128.</param>
/// <returns>A SecureString or null.</returns>
public static SecureString DecryptToSecureString(string cipherText, string key, string ivsalt)
{
return ToSecureString(DecryptToString(cipherText, key, ivsalt));
}
/// <summary>
/// Converts a String to a SecureString.
/// </summary>
/// <param name="value">A String.</param>
/// <returns></returns>A SecureString.
public static SecureString ToSecureString(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
SecureString result = new SecureString();
foreach (var character in value.ToArray<char>())
{
result.AppendChar(character);
}
result.MakeReadOnly();
return result;
}
private static class AesHelper
{
// The public outer or wrapper class "SimpleAes6" works with string inputs and outputs.
// The private inner class "AesHelper" works with byte arrays.
const int KeySize = 256;
const int BlockSize = 128;
const int Iterations = 1000;
private static Aes CreateAesInstance(byte[] key, byte[] iv)
{
var aes = Aes.Create();
aes.KeySize = KeySize;
aes.BlockSize = BlockSize;
var derived = new Rfc2898DeriveBytes(key, iv, Iterations);
aes.Key = derived.GetBytes(aes.KeySize / 8);
aes.IV = derived.GetBytes(aes.BlockSize / 8);
return aes;
}
public static byte[] Encrypt(byte[] bytesToBeEncrypted, byte[] key, byte[] iv)
{
byte[] encryptedBytes = null;
using (var aes = CreateAesInstance(key, iv))
{
using (MemoryStream ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
cs.FlushFinalBlock();
}
encryptedBytes = ms.ToArray();
}
}
return encryptedBytes;
}
public static byte[] Decrypt(byte[] bytesToBeDecrypted, byte[] key, byte[] iv)
{
byte[] decryptedBytes = null;
using (var aes = CreateAesInstance(key, iv))
{
using (MemoryStream ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
cs.FlushFinalBlock();
}
decryptedBytes = TrimZeroPadding(ms.ToArray());
}
}
return decryptedBytes;
}
private static byte[] TrimZeroPadding(byte[] array)
{
if (array == null || array.Length == 0)
{
return null;
}
var lastZeroIndex = array.Length;
for (int i = array.Length - 1; i >= 0; i--)
{
if (array[i] == char.MinValue)
{
lastZeroIndex = i;
}
else
{
break;
}
}
return array.Where((item, index) => index < lastZeroIndex).ToArray();
}
} // private inner class
} // public wrapper class
#pragma warning restore IDE0090 // Use 'new(...)'
#pragma warning restore IDE0063 // Use simple 'using' statement
} // namespace
Example Usage
string key = "Hello, World!";
string ivsalt = "Goodbye, cruel world.";
string plainText = "It's Howdy Doody Time!";
string cipherText = Davin.Cryptography.SimpleAes.Encrypt(plainText, key, ivsalt);
// roundTrip should exactly equal plainText
string roundTrip = Davin.Cryptography.SimpleAes.DecryptToString(cipherText, key, ivsalt);
Review Concerns
Besides all the implied CR concerns (style, naming, practices, etc), I have specific concerns about safety. As mentioned, I am a novice with cryptography. And I had to find a peaceful coexistence with the code base to be used equally in .NET 6 and .NET Framework.
The code works fine but I did struggle originally experimenting with various CipherMode and PaddingMode. Eventually I settled on not specifying them at all with AES. It works, its easy for me to use, and I do not need it to be military grade. Still, I think its a good idea to have it be reviewed so that I can learn from others. There is always room for improvement.