
Cryptographic hashing in .NET is powerful by design, but deliberately low-level. System.Security.Cryptography provides the trusted primitives; applications remain responsible for converting typed values into bytes, controlling encoding and byte order, formatting the resulting digest, and maintaining a consistent surface across ordinary hashes and keyed HMACs.
Cuemon.Security.Cryptography adds that missing application-facing layer.
At its foundation is Hash<TOptions>, which provides typed ComputeHash overloads and standardized result handling. UnkeyedCryptoHash<TAlgorithm> specializes that contract for ordinary cryptographic digests, while KeyedCryptoHash<TAlgorithm> extends the same hierarchy for hash-based message authentication codes.
The result is one consistent API across MD5, SHA-1, SHA-256, SHA-384, SHA-512, SHA-512/256, and their HMAC counterparts. Inputs can be supplied as strings, primitives, streams, byte arrays, or other convertible values, and every operation returns a HashResult with built-in hexadecimal, base64, URL-safe base64, and binary representations.
For the common case, named factory methods reduce the entire operation to a single line.
HashAlgorithm and KeyedHashAlgorithm
Microsoft ships cryptographic hashing in the System.Security.Cryptography namespace. The abstract HashAlgorithm class defines the core contract: ComputeHash(byte[]) and ComputeHash(Stream) return a byte[], while TransformBlock and TransformFinalBlock support incremental hashing.
Concrete implementations follow a predictable naming pattern:
MD5andSHA1for legacy 128-bit and 160-bit digests;SHA256,SHA384, andSHA512for the SHA-2 family;HMACSHA256,HMACSHA384,HMACSHA512,HMACSHA1, andHMACMD5for keyed message authentication codes, all deriving fromKeyedHashAlgorithm, which in turn derives fromHashAlgorithm.
That model is well understood and remains the official baseline. It is also intentionally minimal. The contract is byte-oriented. Callers hashing a string must encode it themselves. Callers hashing an integer, a date, or any other primitive must convert it to bytes first and decide on byte order. The output is a raw byte[] that must be formatted into hexadecimal, base64, or any other representation by hand.
The framework also keeps unkeyed digests and keyed HMACs in parallel hierarchies that share a common ancestor but expose no shared, typed computation surface. And the base class library does not ship a SHA-512/256 implementation at all, even though the algorithm is defined in NIST FIPS PUB 180-4.
None of that is a defect. The framework is deliberately general. But it means every project that hashes typed values, formats digests, or moves between digest and HMAC workflows repeats the same boilerplate.
Hash<TOptions>, UnkeyedCryptoHash<TAlgorithm>, and KeyedCryptoHash<TAlgorithm>
Cuemon.Security.Cryptography introduces a layered hierarchy that wraps the framework types without replacing them.
At the top is Hash<TOptions>, an abstract base that defines a broad, polymorphic ComputeHash surface. Beneath it sits UnkeyedCryptoHash<TAlgorithm>, the base for every unkeyed digest. Beneath that sits KeyedCryptoHash<TAlgorithm>, the base for every HMAC. The sealed implementations are the leaves.
Hash<TOptions>
└─ UnkeyedCryptoHash<TAlgorithm>
├─ MessageDigest5 (MD5, 128-bit)
├─ SecureHashAlgorithm1 (SHA-1, 160-bit)
├─ SecureHashAlgorithm256 (SHA-256, 256-bit)
├─ SecureHashAlgorithm384 (SHA-384, 384-bit)
├─ SecureHashAlgorithm512 (SHA-512, 512-bit)
├─ SecureHashAlgorithm512256 (SHA-512/256, 256-bit)
└─ KeyedCryptoHash<TAlgorithm>
├─ HmacMessageDigest5 (HMACMD5)
├─ HmacSecureHashAlgorithm1 (HMACSHA1)
├─ HmacSecureHashAlgorithm256 (HMACSHA256)
├─ HmacSecureHashAlgorithm384 (HMACSHA384)
└─ HmacSecureHashAlgorithm512 (HMACSHA512)
The hierarchy does three things the baseline does not.
First, it gives hashing a typed entry point. Hash<TOptions> exposes ComputeHash overloads for string, int, long, byte, char, bool, decimal, double, float, DateTime, Enum, Stream, and params IConvertible[], among others. Each overload converts the input to bytes using the configured ConvertibleOptions byte order and delegates to the abstract ComputeHash(byte[]).
Second, it standardizes the result. Every call returns a HashResult that exposes GetBytes(), ToHexadecimalString(), ToBase64String(), ToUrlEncodedBase64String(), and ToBinaryString(), plus a To<T>(converter) escape hatch for custom formatting. The digest is no longer a raw byte[] that every caller must learn to render.
Third, it unifies keyed and unkeyed algorithms under one shape. Because KeyedCryptoHash<TAlgorithm> derives from UnkeyedCryptoHash<TAlgorithm>, an HMAC instance has the same ComputeHash surface and the same HashResult as a plain digest. The only difference is that the keyed hierarchy takes a secret in its constructor.
At its core
UnkeyedCryptoHash<TAlgorithm> is generic over a HashAlgorithm and holds a protected Initializer delegate that creates the algorithm instance on demand.
public abstract class UnkeyedCryptoHash<TAlgorithm> : Hash<ConvertibleOptions>
where TAlgorithm : HashAlgorithm
{
protected UnkeyedCryptoHash(
Func<TAlgorithm> initializer,
Action<ConvertibleOptions> setup)
: base(setup)
{
Validator.ThrowIfNull(initializer);
Initializer = initializer;
}
protected Func<TAlgorithm> Initializer { get; }
public override HashResult ComputeHash(byte[] input)
{
Validator.ThrowIfNull(input);
using var algorithm = Initializer();
return new HashResult(algorithm.ComputeHash(input));
}
}
Two details are worth noting. The constructor forwards the configuration delegate to Hash<ConvertibleOptions> and fails fast when the algorithm initializer is null. Each computation creates and disposes its own HashAlgorithm instance, keeping lifetime management internal and predictable.
KeyedCryptoHash<TAlgorithm> preserves the same polymorphic contract while changing algorithm construction to include a secret key. Its ComputeHash(byte[]) implementation follows the same create, compute, dispose, and wrap pattern, ensuring that keyed and unkeyed operations produce the same HashResult abstraction.
Unkeyed digests
The sealed unkeyed classes mirror the framework algorithms. Each exposes a BitSize constant and a constructor that takes an optional ConvertibleOptions setup delegate.
using Cuemon.Security.Cryptography;
var sha256 = new SecureHashAlgorithm256();
var result = sha256.ComputeHash("codebelt");
Console.WriteLine(result.ToHexadecimalString());
// 64-character lowercase hexadecimal SHA-256 digest
Because the typed overloads live on the base, the same call shape works for non-string inputs:
var digest = new SecureHashAlgorithm512().ComputeHash(42);
var asBase64 = digest.ToBase64String();
When the byte representation of a primitive matters, ConvertibleOptions lets the caller control byte order:
var bigEndian = new SecureHashAlgorithm256(o => o.ByteOrder = Endianness.BigEndian)
.ComputeHash(42);
SHA-512/256 without a separate programming model
SecureHashAlgorithm512256 is the only unkeyed implementation in the hierarchy without a direct framework counterpart. Cuemon.Security.Cryptography therefore provides SHA512256, a managed implementation of the SHA-512/256 algorithm defined by NIST FIPS PUB 180-4.
The algorithm is wrapped by SecureHashAlgorithm512256 using the same UnkeyedCryptoHash<TAlgorithm> abstraction as the framework-backed digests. Consumers therefore gain SHA-512/256 support without learning a separate API or handling its output differently.
var result = new SecureHashAlgorithm512256()
.ComputeHash("codebelt");
Console.WriteLine(result.ToHexadecimalString());
That implementation was added in 9.0.5 and ships for the same target frameworks as the rest of the package, including .NET Standard 2.0.
Note
SHA512256 was also one of my earliest serious experiments with AI-assisted development. In Speed Is Easy — Control Is Hard, I revisit how the implementation exposed an important engineering distinction: a model can produce plausible code quickly, but only a specification, a known-answer test, and a hard feedback loop can establish that the result is actually correct.
Keyed HMACs
The HMAC leaves take a byte[] secret and the same optional options delegate. Apart from the secret, the call shape is identical to the unkeyed case.
using Cuemon.Security.Cryptography;
byte[] secret = Encoding.UTF8.GetBytes("shared-secret");
var hmac = new HmacSecureHashAlgorithm256(secret, null);
var result = hmac.ComputeHash("message body");
Console.WriteLine(result.ToHexadecimalString());
Because HmacSecureHashAlgorithm256 derives from KeyedCryptoHash<HMACSHA256>, which derives from UnkeyedCryptoHash<HMACSHA256>, the same HashResult conversions and the same typed ComputeHash overloads are available. Moving a digest workflow to an HMAC workflow does not require changing how the result is consumed.
Factories
For direct construction, two static factories provide named entry points and sensible defaults. UnkeyedHashFactory.CreateCrypto defaults to SHA-256, and KeyedHashFactory.CreateHmacCrypto defaults to HMAC-SHA256.
var digest = UnkeyedHashFactory.CreateCrypto(); // SecureHashAlgorithm256
var sha512 = UnkeyedHashFactory.CreateCryptoSha512(); // SecureHashAlgorithm512
var sha512256 = UnkeyedHashFactory.CreateCryptoSha512Slash256();
var hmac = KeyedHashFactory.CreateHmacCrypto(secret); // HmacSecureHashAlgorithm256
var hmac512 = KeyedHashFactory.CreateHmacCryptoSha512(secret); // HmacSecureHashAlgorithm512
The factory methods return Hash, so the caller programs against the polymorphic surface and switches algorithms by changing one line.
Correct behavior, limitations, and compatibility
The hierarchy is a wrapping layer. UnkeyedCryptoHash<TAlgorithm>.ComputeHash delegates to the framework HashAlgorithm, so the digest bytes are identical to what SHA256.Create().ComputeHash(input) would produce for the same input. The added value is the typed entry, the HashResult, and the uniform hierarchy — not a reimplementation of the cryptography, with the single exception of SHA512256, which is provided because the framework does not.
A few constraints are worth stating plainly.
ConvertibleOptions controls how primitives and strings are turned into bytes before hashing. That is relevant when hashing values across platforms or comparing digests produced by other libraries: match the byte order and encoding explicitly rather than relying on the default.
MD5 and SHA-1 are cryptographically broken and should not protect new data. The framework itself discourages them. Cuemon.Security.Cryptography keeps MessageDigest5, SecureHashAlgorithm1, and their HMAC counterparts for interoperability scenarios such as legacy checksums, ETag generation, and HTTP Digest authentication — not for new security-sensitive digests. Prefer SecureHashAlgorithm256 or SecureHashAlgorithm512256 for new code.
KeyedCryptoHash<TAlgorithm> constructs the keyed algorithm with Activator.CreateInstance(typeof(TAlgorithm), secret). Callers should pass a secret of the recommended length for the chosen algorithm; the framework HMAC types handle key derivation and padding from there.
The package targets .NET 10, .NET 9, and .NET Standard 2.0, so the hierarchy is available across modern .NET applications and older runtimes that remain on .NET Standard.
Recommended usage
Use the factories for the common case and reach for the concrete types only when the design calls for a specific algorithm or a shared options instance.
- New integrity digests and content fingerprints:
UnkeyedHashFactory.CreateCrypto()(SHA-256) orCreateCryptoSha512Slash256()for a SHA-2 variant with a 256-bit output and a 512-bit internal state. - Message authentication and request signing:
KeyedHashFactory.CreateHmacCrypto(secret)(HMAC-SHA-256) orCreateHmacCryptoSha512(secret)for stronger HMAC. - Legacy interop, ETags, and Digest authentication:
CreateCryptoMd5()orCreateCryptoSha1(), with the understanding that these are not security boundaries. - When the input is already a
byte[]orStream, the abstractComputeHash(byte[])andComputeHash(Stream)overloads remain available on every implementation.
One family, one developer experience
The value of the hierarchy is not a new hashing primitive. It is a consistent developer experience above the primitives already supplied by .NET.
The same typed ComputeHash surface, HashResult conversions, configuration model, and factory pattern apply across every digest and HMAC in the package. A developer who learns the SHA-256 path has effectively learned the SHA-512, SHA-512/256, and HMAC-SHA-256 paths as well.
That consistency removes the small but recurring costs that accumulate across a codebase: encoding strings, converting primitives, controlling byte order, formatting digests, switching between keyed and unkeyed APIs, and teaching every contributor a project-specific hashing convention.
System.Security.Cryptography remains the foundation. Cuemon provides the deliberately opinionated layer above it: a typed, polymorphic, and result-shaped hashing API for both keyed and unkeyed algorithms—with fast feedback and less code.
Sources
- UnkeyedCryptoHash<TAlgorithm> source — cuemon repository
- KeyedCryptoHash<TAlgorithm> source — cuemon repository
- Cuemon.Security.Cryptography on NuGet
- Cuemon for .NET library family
- Cuemon.Security.Cryptography package page
- Cuemon.Security.Cryptography API documentation
- HashAlgorithm class — Microsoft Learn
- KeyedHashAlgorithm class — Microsoft Learn
- NIST FIPS PUB 180-4 — Secure Hash Standard
