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 Rijndael symmetric encryption algorithm
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?
CodePudding user response:
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.
CodePudding user response:
Having read that
RijndaelManaged
is deprecated andAesGcm
(introduced in .NET Core 3.1) is preferred overAesManaged
, I'm trying to implementAesGcm
using this tutorial and this answer.
RijndaelManaged
is deprecated but it is an implementation class. You should have been using Aes.Create
instead for a long time, preferring the no-argument constructor. Generally this will give you the optimized C/C version that can utilize the AES_NI instruction set that is - very likely - part of your CPU. The "Managed" classes are implemented using .NET and are comparably dog slow.
AesGcm
can and should be used directly it seems, and adds authenticity and integrity to the cipher.
using var aes = new AesGcm(passwordBytes);
Yes, no, that's not right. A password is not a key, and AES operates on keys, not passwords. So the part where the key (and possibly IV) are derived from the password is missing. This should be copied from the previous example that you mentioned. However, you should use int nonceSize = AesGcm.NonceByteSizes.MaxSize
as GCM works best for a 96 bit / 12 byte nonce (which is, I suppose what that gives you, in principle GCM doesn't have a functional maximum size for the nonce).
Of course, if performance permits, you can up the number of iterations. Calculating the nonce is fine, as long as you change the salt for each new ciphertext as it should. Beware that your scheme will horribly fail if the salt and nonce do repeat, so use a 128 bit / 16 byte salt just to be sure.
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?
That's correct, but that's because it assumes a fully randomized key. If you have a password, you will have to perform PBKDF2 (which the badly named Rfc2898DeriveBytes
class implements) or another password based key derivation function.
Beware that AES-GCM has an upper limit of 64 GiB (minus a few bytes).
Worse, it requires you to store all the plaintext / ciphertext in a buffer. It may not be the best option for file based encryption in that sense. You can encrypt chunks with GCM as well, but then you need to e.g. perform a HMAC over the authentication tags to make sure that the separately authenticated chunks are not reordered.
AES-CBC HMAC over IV and ciphertext could be another option.