Having read that RijndaelManaged is deprecated and AesGcm (introduced in .NET Core 3.1) is preferred over AesManaged, I'm trying to implement AesGcm using this tutorial and this answer.
Here's my code:
/// Perform AES Encryption, returning the result as a byte array.
/// </summary>
/// <param name="bytesToEncrypt">string, file or data represented as byte array</param>
/// <param name="passwordBytes">A unique password for the encryption (must be 32 bytes?)</param>
/// <returns>The data encrypted</returns>
public byte[] EncryptData(byte[] bytesToEncrypt, byte[] passwordBytes)
{
// Based on https://stackoverflow.com/questions/60889345/using-the-aesgcm-class/60891115#60891115
// Get parameter sizes
int nonceSize = AesGcm.NonceByteSizes.MaxSize;
int tagSize = AesGcm.TagByteSizes.MaxSize;
int cipherSize = bytesToEncrypt.Length;
// We write everything into one big array for easier encoding
int encryptedDataLength = 4 + nonceSize + 4 + tagSize + cipherSize;
Span<byte> encryptedData = encryptedDataLength < 1024
? stackalloc byte[encryptedDataLength]
: new byte[encryptedDataLength].AsSpan();
// Copy parameters
BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(0, 4), nonceSize);
BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4), tagSize);
var nonce = encryptedData.Slice(4, nonceSize);
var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize);
var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);
// Generate secure nonce
RandomNumberGenerator.Fill(nonce);
// Encrypt
using (var aes = new AesGcm(passwordBytes))
{
aes.Encrypt(nonce, bytesToEncrypt.AsSpan(), cipherBytes, tag);
}
return encryptedData.ToArray();
}
/// <summary>
/// Takes in an AES encrypted byte array, decrypts it and returns the resulting unencrypted byte array.
/// </summary>
/// <param name="encryptedBytes">A string, file or object represented as a byte array that's previously been encrypted.</param>
/// <param name="passwordBytes">The password used to encrypt the data. </param>
/// <returns></returns>
public byte[] DecryptData(byte[] encryptedBytes, byte[] passwordBytes)
{
// Decode
Span<byte> encryptedData = encryptedBytes.AsSpan();
// Extract parameter sizes
int nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(0, 4));
int tagSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4));
int cipherSize = encryptedData.Length - 4 - nonceSize - 4 - tagSize;
// Extract parameters
var nonce = encryptedData.Slice(4, nonceSize);
var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize);
var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);
// Decrypt
Span<byte> plainBytes = cipherSize < 1024
? stackalloc byte[cipherSize]
: new byte[cipherSize];
using (var aes = new AesGcm(passwordBytes))
{
aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
}
// Convert plain bytes back into string
return plainBytes.ToArray();
}
One thing I've noticed, is there there doesn't seem to be a place for iterations.
For example, in AesManaged, I've always iteratated through like the following, as the iterations make attacks more complicated. I've followed a similar pattern for password hashing:
// Set Symmetric encryption algorithm
// Based on http://stackoverflow.com/questions/27645527/aes-encryption-on-large-files
var AES = Aes.Create("AesManaged");
AES.KeySize = 256;
AES.BlockSize = 128;
AES.Padding = PaddingMode.PKCS7;
//http://stackoverflow.com/questions/2659214/why-do-i-need-to-use-the-rfc2898derivebytes-class-in-net-instead-of-directly
//"What it does is repeatedly hash the user password along with the salt." High iteration counts.
var key = new Rfc2898DeriveBytes(passwordBytes, salt, 100000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
...
I appreciate the nonce
will have an impact, but everything I've done before has relied on multiple iterations, so it seems odd not to be doing this in AesGcm
.
AesGcm doesn't seem to have a means of iterating. Should I be iterating a part somewhere? If I should be iterating, how should I do this please?
The first important point is the your second sample generates the IV from the password. DO NOT DO THIS! The IV needs to be unique for each encryption, particularly if the key is re-used. You're doing this properly in your AES-GCM sample, where you generate a random nonce which is sent at the start of the ciphertext.
One basic example: imagine you encrypt two messages which start with the same text, using the same shared key. If you use the same IV for both, the two ciphertexts will also be identical. You do not want this.
AES requires a key which is exactly the right length. Rfc2898DeriveBytes
is used to turn an arbitrary-length password into a key which has the correct number of bytes. In your second sample, passwordBytes
may be any length.
In your AES-GCM sample, passwordBytes
is passed directly to the AesGcm
constructor. Therefore, passwordBytes
must already be exactly the right length.
If you wish, you can use Rfc2898DeriveBytes
on passwordBytes
before passing the resulting key to the AesGcm
constructor. This would let you use a passwordBytes
of any length.