The goal of the code below is to:
- Support authenticated encryption using AES in the CBC mode and using HMAC SHA.
- Support encryption and decryption of data of size larger than memory (potentially).
Please criticize :)
public class AesCbcThenHmacSha : IDisposable
{
private readonly Func<HMAC> hmacShaTransformFactory;
private readonly AesCryptoServiceProvider aesProvider;
private bool disposed;
public AesCbcThenHmacSha(byte[] aesKey, byte[] aesIv, byte[] macKey)
{
if (aesKey == null) throw new ArgumentNullException(nameof(aesKey));
if (aesIv == null) throw new ArgumentNullException(nameof(aesIv));
if (macKey == null) throw new ArgumentNullException(nameof(macKey));
// key size
// --------
// the key size property is set based on the provided
// key. the validation of length is also performed
// by the AesCryptoServiceProvider.
// block size
// ----------
// there is only one valid block size for AES and
// it does not need to be specified explicitly. it
// is 128 by default (and cannot be changed to any
// different value).
// initialization vector
// ---------------------
// the iv has to match the block size, so validation
// of the length is also performed beneath.
this.aesProvider = new AesCryptoServiceProvider
{
Key = aesKey,
IV = aesIv,
Padding = PaddingMode.PKCS7,
Mode = CipherMode.CBC
};
this.hmacShaTransformFactory = CreateHmacShaTransform(macKey);
}
private static Func<HMAC> CreateHmacShaTransform(byte[] key)
{
var bits = key.Length * 8;
switch (bits)
{
case 256: return () => new HMACSHA256(key);
case 384: return () => new HMACSHA384(key);
case 512: return () => new HMACSHA512(key);
default:
throw new ArgumentException($"the {bits}-bit HMAC SHA is not supported");
}
}
public void Encrypt(Stream inputStream, Stream outputStream, out byte[] hmacSha)
{
if (inputStream == null) throw new ArgumentNullException(nameof(inputStream));
if (outputStream == null) throw new ArgumentNullException(nameof(outputStream));
var hmacShaTransform = this.hmacShaTransformFactory.Invoke();
var aesTransform = this.aesProvider.CreateEncryptor();
var hmacShaStream = new CryptoStream(outputStream, hmacShaTransform, CryptoStreamMode.Write);
var aesStream = new CryptoStream(hmacShaStream, aesTransform, CryptoStreamMode.Write);
inputStream.CopyTo(aesStream);
aesStream.FlushFinalBlock();
hmacSha = hmacShaTransform.Hash;
hmacShaTransform.Dispose();
}
public void Decrypt(Stream inputStream, Stream outputStream, byte[] expectedHmacSha)
{
if (inputStream == null) throw new ArgumentNullException(nameof(inputStream));
if (outputStream == null) throw new ArgumentNullException(nameof(outputStream));
if (expectedHmacSha == null) throw new ArgumentNullException(nameof(expectedHmacSha));
using (var hmacShaTransform = this.hmacShaTransformFactory.Invoke())
using (var hmacShaStream = new CryptoStream(Stream.Null, hmacShaTransform, CryptoStreamMode.Write))
{
inputStream.CopyTo(hmacShaStream);
hmacShaStream.FlushFinalBlock();
var hmacSha = hmacShaTransform.Hash;
if (!hmacSha.SequenceEqual(expectedHmacSha))
{
throw "message authentication code does not match the expected value".Security();
}
}
inputStream.Position = 0;
var aesCryptoTransform = this.aesProvider.CreateDecryptor();
var aesStream = new CryptoStream(outputStream, aesCryptoTransform, CryptoStreamMode.Write);
inputStream.CopyTo(aesStream);
aesStream.FlushFinalBlock();
}
public void Dispose()
{
if (!this.disposed)
{
this.aesProvider.Dispose();
this.disposed = true;
}
}
}