Guide

HMAC and verify

Key a digest with HMAC and compare an expected digest byte for byte. Hex ignores case and base64 never does

HMAC is a key away

Pass key and the same hash() computes an HMAC. The result says so in operation.

ts
import { create } from "@agntn/hashes";

const tag = create("sha256").hash("message", { key: "secret" });
tag.digest; // "8b5f48702995c1598c573db1e21866a9b825d4a794d169d7060a03605796360b"
tag.operation; // "hmac"

The key is text read as UTF-8, or a Uint8Array. The CLI and the tools can't pass bytes, so a binary key like a BIP32 chain code goes in as hex or base64 with --key-encoding or keyEncoding. HMAC exists for the algorithms built on a block, SHA-2, SHA-3, Keccak-256, BLAKE2b and BLAKE2s, RIPEMD-160, MD5 and SHA-1. Everything else, BLAKE3 and the Bitcoin compositions included, throws instead of pretending:

ts
create("blake3").hash("message", { key: "secret" });
// HashError: [blake3] blake3 has no HMAC mode

BLAKE3 has its own keyed mode, and it isn't HMAC. Handing you a plain BLAKE3 digest because it ignored the key would be the worst possible answer, so you get an error.

The HMAC column on the algorithm list says which is which, read from info().hmac.

Verify compares bytes

digestMatches(result, expected) decodes both digests in the result's encoding and compares the bytes. It goes through every byte even after the first difference, so the time it takes doesn't tell where they differ.

ts
import { create, digestMatches } from "@agntn/hashes";

const hex = create("sha256").hash("abc");
digestMatches(hex, "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD"); // true

const b64 = create("sha256").hash("abc", { encoding: "base64" });
digestMatches(b64, "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="); // true
digestMatches(b64, "ungwv48bz+pbqudexa4ii7adyaowf3qctbd/yfiafa0="); // false

Hex ignores case because A and a are the same nibble. Base64 doesn't, because they aren't the same byte. Lowercasing both sides before comparing is the classic shortcut, and for base64 it's a bug that says yes to a digest that isn't yours. Surrounding whitespace is trimmed. A string that isn't valid in the encoding is a plain false, not a crash.

binary can't be compared as text, and asking is an InvalidOptionError.

From the terminal

shell
hashes verify sha256 abc "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=" -e base64
# MATCH sha256 ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=

hashes verify sha256 abc "ungwv48bz+pbqudexa4ii7adyaowf3qctbd/yfiafa0=" -e base64
# MISMATCH sha256
#   expected ungwv48bz+pbqudexa4ii7adyaowf3qctbd/yfiafa0=
#   actual   ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=

A mismatch exits with 1, so hashes verify ... && deploy does what it reads like. -e is the encoding of the expected digest.

hashes hmac sha256 message secret prints the tag alone. An algorithm without an HMAC mode is refused before anything is hashed.

For a KDF

scrypt and PBKDF2 draw a random salt when you don't give one, so verifying without the salt could only ever say no. hashes verify and hash_verify refuse to run without it, with an error that tells you which salt they want. In the library you pass the same salt and costs to hash() and then digestMatches. More on Salted KDFs.