Coding Stephan

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.

Compute file hash asynchronously

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.