Looking for code reviews of these two encryption methods. And if GCM is better or worse than CBC with HMAC.
AES-GCM
public static async Task<byte[]> EncryptAsyncV2(byte[] plainText, byte[] password, byte[] nonce, byte[] salt) { try { if (plainText == null) throw new ArgumentException(@"Value was empty or null.", nameof(plainText)); if (password == null) throw new ArgumentException(@"Value was empty or null.", nameof(password)); if (salt == null) throw new ArgumentException(@"Value was empty or null.", nameof(salt)); if (nonce == null) throw new ArgumentException(@"Value was empty or null.", nameof(nonce)); var cipherText = new byte[plainText.Length]; var tag = new byte[TagLen]; using (var argon2 = new Argon2id(password)) { argon2.Salt = salt; argon2.DegreeOfParallelism = Environment.ProcessorCount * 2; argon2.Iterations = Iterations; argon2.MemorySize = (int)MemorySize; var key = await argon2.GetBytesAsync(KeySize); using var aesGcm = new AesGcm(key, TagLen); aesGcm.Encrypt(nonce, plainText, cipherText, tag); Array.Clear(key, 0, key.Length); } cipherText = tag.Concat(nonce.Concat(cipherText)).ToArray(); return cipherText; } catch (CryptographicException ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } catch (ArgumentNullException ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } catch (Exception ex) { Array.Clear(password!, 0, password!.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } } public static async Task<byte[]> DecryptAsyncV2(byte[] cipherText, byte[] password, byte[] salt) { try { if (cipherText == null) throw new ArgumentException(@"Value was empty or null.", nameof(cipherText)); if (password == null) throw new ArgumentException(@"Value was empty or null.", nameof(password)); if (salt == null) throw new ArgumentException(@"Value was empty or null.", nameof(salt)); using var argon2 = new Argon2id(password); argon2.Salt = salt; argon2.DegreeOfParallelism = Environment.ProcessorCount * 2; argon2.Iterations = Iterations; argon2.MemorySize = (int)MemorySize; var key = await argon2.GetBytesAsync(KeySize); using var aesGcm = new AesGcm(key, TagLen); var tag = new byte[TagLen]; var nonce = new byte[NonceSize]; var cipherResult = new byte[cipherText.Length - nonce.Length - tag.Length]; Buffer.BlockCopy(cipherText, 0, tag, 0, tag.Length); Buffer.BlockCopy(cipherText, tag.Length, nonce, 0, nonce.Length); Buffer.BlockCopy(cipherText, tag.Length + nonce.Length, cipherResult, 0, cipherResult.Length); var plainText = new byte[cipherResult.Length]; aesGcm.Decrypt(nonce, cipherResult, tag, plainText); Array.Clear(key, 0, key.Length); return plainText; } catch (CryptographicException ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } catch (ArgumentNullException ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } catch (Exception ex) { Array.Clear(password!, 0, password!.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } #pragma warning restore } The nonce is generated with the RandomNumberGenerator class using this method
public static byte[] RndByteSized(int size) { var buffer = new byte[size]; RndNum.GetBytes(buffer); return buffer; } AES-CBC with HMAC
public static async Task<byte[]?> EncryptAsync(byte[]? plainText, byte[]? password, byte[]? iv, byte[]? salt) { try { if (plainText == null) throw new ArgumentException(@"Value was empty or null.", nameof(plainText)); if (password == null) throw new ArgumentException(@"Value was empty or null.", nameof(password)); if (salt == null) throw new ArgumentException(@"Value was empty or null.", nameof(salt)); if (iv == null) throw new ArgumentException(@"Value was empty or null.", nameof(iv)); using var aes = Aes.Create(); aes.BlockSize = BlockBitSize; aes.KeySize = KeyBitSize; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; using var argon2 = new Argon2id(password); argon2.Salt = salt; argon2.DegreeOfParallelism = Environment.ProcessorCount * 2; argon2.Iterations = Iterations; argon2.MemorySize = (int)MemorySize; var key = await argon2.GetBytesAsync(KeySize); var hmacKey = await argon2.GetBytesAsync(64); byte[] cipherText; using (var encryptor = aes.CreateEncryptor(key, iv)) using (var memStream = new MemoryStream()) { await using (var cryptoStream = new CryptoStream(memStream, encryptor, CryptoStreamMode.Write)) { using (var cipherStream = new MemoryStream(plainText)) { cipherStream.CopyTo(cryptoStream, (int)cipherStream.Length); cipherStream.Flush(); cryptoStream.FlushFinalBlockAsync(); } } cipherText = memStream.ToArray(); } Array.Clear(key, 0, key.Length); var tag = new byte[64]; using (var hmac = new HMACSHA3_512(hmacKey)) { var prependItems = new byte[cipherText.Length + iv.Length]; Buffer.BlockCopy(iv, 0, prependItems, 0, iv.Length); Buffer.BlockCopy(cipherText, 0, prependItems, iv.Length, cipherText.Length); tag = hmac.ComputeHash(prependItems); var authenticatedBuffer = prependItems.Length + tag.Length; var authenticatedBytes = new byte[authenticatedBuffer]; Buffer.BlockCopy(prependItems, 0, authenticatedBytes, 0, prependItems.Length); Buffer.BlockCopy(tag, 0, authenticatedBytes, prependItems.Length, tag.Length); Array.Clear(hmacKey, 0, hmacKey.Length); return authenticatedBytes; } } catch (CryptographicException ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } catch (ArgumentNullException ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } catch (Exception ex) { Array.Clear(password, 0, password.Length); ErrorLogging.ErrorLog(ex); return Array.Empty<byte>(); } }