A BlockHasher helper class

There are a few instances in which you'll need to hash a combination of data. You might resort to creating one big string and hashing that. It has a clear disadvantage from a memory and processing point of few. It might even be impractical when files or streams are involved. That's why I created a BlockHasher utility class that helps to generate these types of hashes.
HashAlgorithm
.NET offers the following through the HashAlgorithm class:
Wanted: more features
Basically you want the following features in this scenario:
To make things more readable, you might want to return the hash as a hexadecimal string or - a shorter, but less frequently used - base64 string. I find myself always using the same lines of code, so I added it to the class.
BlockHasher
Without further ado, let's see the class:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// Helps with block hashing.
/// </summary>
public class BlockHasher : IDisposable
{
private HashAlgorithm _hasher;
/// <summary>
/// Initializes a new instance of the <see cref = "BlockHasher"/> class.
/// </summary>
/// <param name = "name">The name.</param>
public BlockHasher(string name)
{
if (String.IsNullOrEmpty(name))
{
throw new ArgumentNullException("name");
}
_hasher = HashAlgorithm.Create(name);
}
/// <summary>
/// Initializes a new instance of the <see cref = "BlockHasher"/> class.
/// </summary>
/// <param name = "algorithm">The algorithm.</param>
public BlockHasher(HashAlgorithm algorithm)
{
if (algorithm == null)
{
throw new ArgumentNullException("algorithm");
}
_hasher = algorithm;
}
/// <summary>
/// Indicates how to format the string.
/// </summary>
public enum StringFormat
{
Hexadecimal,
Base64
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
_hasher.Dispose();
}
/// <summary>
/// Gets the hash.
/// </summary>
/// <returns>A byte array with the hash.</returns>
public byte[] GetHash()
{
_hasher.TransformFinalBlock(new byte[0], 0, 0);
return _hasher.Hash;
}
/// <summary>
/// Gets the string hash.
/// </summary>
/// <param name = "format">The format.</param>
/// <returns>The hash.</returns>
public string GetStringHash(StringFormat format = StringFormat.Hexadecimal)
{
var hash = GetHash();
switch (format)
{
case StringFormat.Hexadecimal:
{
string result = "";
for (int i = 0; i < hash.Length; i++)
{
result += hash[i].ToString("x2");
}
return result;
}
case StringFormat.Base64:
{
return Convert.ToBase64String(hash);
}
default:
{
throw new NotSupportedException();
}
}
}
/// <summary>
/// Transforms the specified stream.
/// </summary>
/// <param name = "stream">The stream.</param>
/// <param name = "bufferSize">Size of the buffer.</param>
public void Transform(Stream stream, int bufferSize = 8 * 1024)
{
if (stream == null)
{
throw new ArgumentNullException("stream");
}
byte[] buffer = new byte[bufferSize];
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
Transform(buffer, 0, read);
}
}
/// <summary>
/// Transforms the specified string. Assumes UTF8 encoding.
/// </summary>
/// <param name = "str">The string.</param>
public void Transform(string str)
{
Transform(str, Encoding.UTF8);
}
/// <summary>
/// Transforms the specified string using the specified encoding.
/// </summary>
/// <param name = "str">The string.</param>
/// <param name = "encoding">The encoding.</param>
public void Transform(string str, Encoding encoding)
{
var bytes = encoding.GetBytes(str);
Transform(bytes);
}
/// <summary>
/// Transforms the specified buffer.
/// </summary>
/// <param name = "buffer">The buffer.</param>
public void Transform(byte[] buffer)
{
Transform(buffer, 0, buffer.Length);
}
/// <summary>
/// Transforms the specified buffer.
/// </summary>
/// <param name = "buffer">The buffer.</param>
/// <param name = "offset">The offset.</param>
/// <param name = "length">The length.</param>
public void Transform(byte[] buffer, int offset, int length)
{
_hasher.TransformBlock(buffer, offset, length, null, 0);
}
/// <summary>
/// Transforms the specified stream asynchronously.
/// </summary>
/// <param name = "stream">The stream.</param>
/// <param name = "bufferSize">Size of the buffer.</param>
/// <returns>The task.</returns>
public async Task TransformAsync(Stream stream, int bufferSize = 8 * 1024)
{
byte[] buffer = new byte[bufferSize];
int read;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
Transform(buffer, 0, read);
}
}
}
API Signature Hashing
Lots of API's use a HMAC SHA1 signature to authenticate messages. I've created some example code on how the BlockHasher can be used to generate the signature:
[TestCategory("UnitTest")]
[TestMethod]
public void BlockHasher_ApiRequest_HMACSHA1()
{
//api information
string apiKey = "DvTVukOITB5GJ5r79IEy3J9LALZ1LLex";
string clientSecret = "5d18SSM38x1lGjMD5qCX1FJGsw4jJ12t";
//request data
Dictionary<string, string> request = new Dictionary<string, string>();
request.Add("Message", "Hello world!");
request.Add("PublishDate", "1984-09-12");
request.Add("Tags", "first,message,ever");
request.Add("Active", "true");
string signature = null;
using (var algorithm = new HMACSHA1(Encoding.ASCII.GetBytes(clientSecret)))
{
var hasher = new BlockHasher(algorithm);
var orderedKeys = request.Keys.OrderBy(k => k);
//hash keys
foreach (var k in orderedKeys)
{
hasher.Transform(k);
hasher.Transform(",");
}
//hash values
foreach (var k in orderedKeys)
{
hasher.Transform(request[k]);
hasher.Transform(",");
}
hasher.Transform(apiKey);
signature = hasher.GetStringHash(BlockHasher.StringFormat.Base64);
}
Assert.AreEqual("279y647EmEXFNFH2ZtNesIc6Skw=", signature);
}
Async stream hashing
Hashing big files using async streams is pretty easy:
public async Task<string> HashFile(string path, string algorithm = "md5")
{
using (var file = File.OpenRead(path))
{
using (var blockhasher = new BlockHasher("md5"))
{
await blockhasher.TransformAsync(file);
return blockhasher.GetStringHash();
}
}
}
Wrap up
So that's it. Be sure to visit the class on GitHub if you think it should be changed / expanded.