Compute file hash in C# asynchronously
In my recent quest I’ve been building a tool that publishes any apps from winget to Intune, called WingetIntune. One of the things that was still on the todo list was to compute and check the hash of the installer. We wouldn’t want to package a corrupted or maliciously changed installer, right?
Since I’m building my application with the cloud in mind, using synchronous code is a big no-no. So I went looking for a way to compute the hash of a file asynchronously. Only to find out there is no such thing in .NETStandard 2.0
which I was targeting.
The synchronous way
The synchronous way is pretty easy, you just call the ComputeHash
method on the HashAlgorithm
class. This class is abstract, so you’ll need to use one of the implementations, like SHA256
or SHA512
. The code looks like this:
using(var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None))
using (var sha256 = SHA256.Create())
{
var hashBytes = sha256.ComputeHash(fileStream);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
The problem with this code is that it’s synchronous, which is announced to be blocked back in 2019. So I went looking for an asynchronous way.
The asynchronous way
The asynchronous way is a bit more complicated, but not much. You’ll need to use the HashAlgorithm
class again, but this time you’ll need to use the TransformBlock
and TransformFinalBlock
methods. The code looks like this:
var cancellationToken = CancellationToken.None; // Some cancellation token
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 4096, useAsync: true))
using (var sha256 = SHA256.Create())
{
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) != 0)
{
sha256.TransformBlock(buffer, 0, bytesRead, null, 0);
}
sha256.TransformFinalBlock(buffer, 0, 0);
return BitConverter.ToString(sha256.Hash).Replace("-", "").ToLowerInvariant();
}
This code does the asynchronous reading of the file, and then calls the TransformBlock
method to compute the hash. The TransformFinalBlock
method is called after the file is read, to finalize the hash. The TransformBlock
method can be called multiple times, but the TransformFinalBlock
method can only be called once.
The FileStream
is also created with the useAsync
parameter set to true
, this makes sure the system is signalled that the stream is used asynchronously. This is not required, but it’s a good practice.
Bonus: The ComputeHashAsync method
Since I’m a lazy efficient developer, I wanted to have a method that does all the work for me. So I created the following extension method:
public static class HashExtensions {
public static async Task<string> ComputeHashAsync(this HashAlgorithm hashAlgorithm, Stream fileStream, CancellationToken cancellationToken = default)
{
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) != 0)
{
hashAlgorithm.TransformBlock(buffer, 0, bytesRead, null, 0);
}
hashAlgorithm.TransformFinalBlock(buffer, 0, 0);
// In some cases you might want to save the hash in some other encoding, in that case it might be better to return the byte array instead of the string.
return BitConverter.ToString(hashAlgorithm.Hash).Replace("-", "").ToLowerInvariant();
}
}
This method can be called like this:
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 4096, useAsync: true))
using (var sha256 = SHA256.Create())
{
return await sha256.ComputeHashAsync(fileStream, cancellationToken);
}
Conclusion
Soon my WingetIntune app will validate the hash of the installer it’s packaging for Intune. And if it fails validation you’ll get a big error message.