Repository navigation
No keyed-hash provider, so authenticating ciphertext requires leaving Essentials #5
Description
Every hash provider in Essentials is unkeyed, so there is no way to compute a message authentication code through the abstraction. Verified against the shipped ktsu.Essentials 2.0.0 assembly: no type or member mentioning a keyed hash or HMAC exists anywhere in it.
Why this matters more than a missing algorithm
IEncryptionProvider provides confidentiality but not integrity, and its shape makes that permanent for any implementation: the interface has no authentication tag anywhere in its surface.
byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv);
byte[] Decrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv);There is no tag to return from Encrypt and none to pass to Decrypt, so an authenticated mode such as AES-GCM cannot be expressed through this interface at all. AesEncryptionProvider accordingly builds on Aes.Create() defaults, which is CBC with PKCS7 padding, and CBC ciphertext is malleable: an attacker who can modify it can make predictable changes to the plaintext, and a decrypt-then-parse caller becomes a padding oracle.
That combination is the problem. A caller who reaches for "the encryption provider" reasonably expects tampering to be detected, gets no indication from the API that it is not, and has no keyed-hash provider available to add authentication with. The only route is out of Essentials entirely, into the base class library.
What this cost in practice
ktsu-dev/GitLfsCache encrypts a token that travels in a URL and carries an upstream credential plus the object id that selects which bytes get served, so tamper detection is not optional there. The codec had to be encrypt-then-authenticate, and the authentication half could not use Essentials:
// Essentials for the encryption
byte[] ciphertext = encryption.Encrypt(plaintext, key.Encryption, iv);
// Base class library for the authentication, because every Essentials hash provider is unkeyed
HMACSHA256.HashData(key.Authentication, buffer.AsSpan(0, authenticatedLength), buffer.AsSpan(authenticatedLength));HKDF is also taken from the base class library to derive the separate encryption and authentication keys.
Suggested direction
A keyed hash provider, mirroring the shape of the existing ones:
public interface IKeyedHashProvider
{
public int HashLengthBytes { get; }
public bool TryHash(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination, out int written);
public bool TryHash(ReadOnlySpan<byte> key, Stream data, Span<byte> destination, out int written);
public byte[] Hash(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data);
}with ktsu.Essentials.KeyedHashProviders.HmacSha256 and siblings for the other SHA sizes. A fixed-time comparison helper alongside it would be worth having, since comparing tags with == is the obvious next mistake and CryptographicOperations.FixedTimeEquals is easy to forget.
Documenting the encryption provider's guarantee. Whatever happens with keyed hashing, the XML documentation on IEncryptionProvider should say plainly that it provides confidentiality only, that ciphertext is not tamper-evident, and that a caller who needs integrity must authenticate the ciphertext separately. The obfuscation providers already carry a clear warning that they are not encryption; this is the same class of expectation gap one level up.
An authenticated provider, if the interface can accommodate one. Supporting AES-GCM would need a tag in the surface, which is a breaking change to IEncryptionProvider, so it may belong behind a separate IAuthenticatedEncryptionProvider rather than a modification. Worth considering, because encrypt-then-authenticate assembled by hand has more moving parts than most callers should be taking on.
Happy to send a pull request for the keyed hash provider if the shape above looks right.
Activity
matt-edmondson commentedon Aug 19, 2026
Related: #6 covers the other place
IHashProvidercame up short for the same project, which is hashing a large stream without blocking or reading it twice.