Summary#
Crypto.HmacSha256Hex(key, message) returns the HMAC-SHA-256 tag of message under key as a 64-character
lowercase hex string. Unlike a plain hash, it is keyed: only someone holding the key can produce a valid tag, so
the tag proves the message came from a key holder and was not altered.
Crypto.FixedTimeEquals(a, b) compares two tags in constant time. Verify every received tag with it — never with
==.
Signature#
Crypto.HmacSha256Hex(<string> key, <string> message) -> string
Crypto.FixedTimeEquals(<string> a, <string> b) -> boolDescription#
Use HMAC whenever you must trust a message that arrived from outside: a webhook payload, a signed URL parameter, an API callback. A plain hash cannot do this job — an attacker who rewrites the payload just recomputes its hash. Because the HMAC tag depends on a key only you and the sender know, it cannot be recomputed by a third party.
The key and the message are distinct roles and are not interchangeable: swapping them produces a different
(and wrong) tag. The key should come from configuration or the secret store, never a literal in source.
Verify with FixedTimeEquals, never ==#
This is the part that is easy to get wrong, so it is worth being precise about. String equality short-circuits: it returns as soon as it hits the first differing byte. That means comparing a wrong tag that shares a long prefix with the correct one takes measurably longer than one that differs immediately. An attacker who can submit many guesses and time the responses can exploit that difference to discover the correct tag one byte at a time — and then forge a valid signature, defeating the whole mechanism.
Crypto.FixedTimeEquals compares the full length regardless of where the values differ, so the time it takes reveals
nothing about how close a guess was. A length mismatch simply returns false (it does not throw).
The rule is unconditional: any value being checked against a secret — an HMAC tag, a signature, a token — is
compared with Crypto.FixedTimeEquals.
Both functions run in memory. Crypto.HmacSha256Hex has no SQL push-down form (a constant-time comparison is
meaningless once a database is doing the matching), so using them in a query predicate is a compile error rather than
a silently weaker check.
Examples#
Verifying a signed webhook — the canonical use, and the canonical mistake it prevents:
// The sender signs the payload with the shared key; we recompute the tag and compare.
// FixedTimeEquals is what makes this safe to expose to an attacker who can retry.
bool IsAuthenticWebhook(string payload, string receivedSignature, string sharedKey) {
var expected = Crypto.HmacSha256Hex(sharedKey, payload);
return Crypto.FixedTimeEquals(expected, receivedSignature);
}
// WRONG — never do this. `==` short-circuits on the first differing byte, leaking through
// its timing how much of the tag a guess got right, which lets an attacker forge one:
// return Crypto.HmacSha256Hex(sharedKey, payload) == receivedSignature;See also#
- Crypto.Sha256Hex — the unkeyed secure hash (integrity, not authentication)
- Crypto.Md5Hex — the non-adversarial checksum, and why it is not a security primitive