Guide

Hashing

Pick an algorithm by name and hash text or bytes. Hex and base64 and base64url or raw bytes out

One method

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

const sha256 = create("sha256");

sha256.hash("abc").digest;
// "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
sha256.hash("abc", { encoding: "base64" }).digest;
// "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
sha256.hash("abc", { encoding: "base64url" }).digest;
// "ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0"
sha256.hash("abc", { encoding: "binary" }).digest;
// Uint8Array(32)

That's the whole API for most people. hash(input, options) on any algorithm, the same result shape back. hex is the default. base64url drops the padding, the way JWTs write it. binary hands you the raw Uint8Array, which is what you want before you feed the digest into another hash.

Those four are the whole list. Anything else throws an InvalidOptionError. TypeScript stops "HEX" at compile time, and plain JavaScript gets the error at runtime. A loud error beats a base64 digest wearing a hex label.

Text or bytes

A string is read as UTF-8. A Uint8Array is hashed as it is. The difference matters the moment your input is already bytes written as hex, like a public key:

ts
const key = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";

create("hash160").hash(key).digest;
// "b170e26bb62fb9dbfa28bb2f5e320c96459a2bed", the 66 characters, hashed as text
create("hash160").hash(Uint8Array.fromHex(key)).digest;
// "751e76e8199196d454941c45d1b3a323f1433bd6", the 33 bytes, which is what Bitcoin hashes

The first one is a perfectly valid digest of a string nobody meant. That's the classic bug, and it's silent. The library takes bytes, so pass bytes. The CLI and the agent tools can't pass a Uint8Array, so they take --input-encoding hex and inputEncoding: "hex" instead and decode it for you. See CLI and Agents.

Non-ASCII text is fine, UTF-8 is UTF-8. "żółw" and new TextEncoder().encode("żółw") give the same digest.

Many rounds

"SHA-512 of the answer, ten thousand times." Puzzle pages love that one. rounds runs the loop for you:

ts
const sha256 = create("sha256");

sha256.hash("abc", { rounds: 2 }).digest;
// "4f8b42c22dd3729b519ba6f68d2da7cc5b2d606d05daed5ad5128cc03e6c6358"
sha256.hash("abc", { rounds: 2, chain: "hex" }).digest;
// "dfe7a23fefeea519e9bbfdd1a6be94c4b2e4529dd6b7cbea83f9959c2621b13c"

Same input, same count, two answers. Why? Round two has to hash something. chain: "bytes" is the default and hashes the raw digest. chain: "hex" hashes the lowercase text that spells it. Both recipes are out there. A page rarely says which one it means, so try both before you doubt your answer.

Does the first digest look familiar? It's hash256. Bitcoin has run two rounds of SHA-256 over bytes since 2009.

Past one round, options in the result names rounds and chain. One logged result is enough to repeat it. Every fixed-length digest takes them. HMAC and the KDFs don't, since they have their own cost. More rounds there is an error, not a quiet single pass.

A pile of inputs

Twelve words, twelve digests? Don't write the loop. hash eats a list too. The options go once:

ts
create("sha256").hash(["hello", "world"]).map((result) => result.digest);
// ["2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
//  "486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7"]

A string gets one result. A list gets an array, same order. Underneath it's a map, nothing clever. But every algorithm does it, your own FixedHash or Kdf included. A KDF without a salt draws a fresh one per input. Two users with the same password still get two different hashes. Isn't that the whole point of a salt?

Bytes in, bytes out

Building a wallet or an address codec? Then you don't want a result object. You have bytes, you want bytes, and you'd rather not ship every algorithm here to hash with one.

ts
import { Sha512Hasher, blake2b, hash160, hmac, pbkdf2 } from "@agntn/hashes";

const key = Uint8Array.fromHex("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
hash160(key).toHex(); // "751e76e8199196d454941c45d1b3a323f1433bd6"
blake2b(key, 20); // Uint8Array(20), any length from 1 to 64

// The BIP39 test vector, then its BIP32 master key
const text = new TextEncoder();
const sha512 = () => new Sha512Hasher();
const words = "abandon ".repeat(11) + "about";
const seed = pbkdf2(sha512, text.encode(words), text.encode("mnemonic"), 2048, 64);
// "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1..."
hmac(sha512, text.encode("Bitcoin seed"), seed);
// Uint8Array(64), the key "1837c1be8e2995ec..." and then the chain code

Each one takes a Uint8Array and gives you a new one. Here's the lineup:

  • Digests: sha224, sha256, sha384, sha512, ripemd160, hash160, hash256, keccak256, sha3_256, sha3_512, blake256 and blake2b.
  • Checksums: crc32, crc64, crc24, crc16Xmodem, adler32 and xxhash.
  • Old formats that still ask for them: sha1 and md5.

blake2b also takes a personalization of up to 16 bytes as its third argument, because Zcash's F4Jumble tags every call with one. The checksums take their variant the same way the registry does, as an argument: crc32(bytes, "bzip2"), xxhash(bytes, 32, seed).

Got a message that stops inside a byte? sha256Bits(bytes, 7) hashes only the first 7 bits, packed from the top. SHA-256 has the details.

hmac, pbkdf2 and hkdf take a function that makes a hasher: Sha256Hasher, Sha512Hasher, Ripemd160Hasher, Blake2bHasher, Sha1Hasher or Md5Hasher. So do hkdfExtract and hkdfExpand, the two halves of hkdf, for a protocol that calls only one. And so does evpBytesToKey, for whatever openssl enc and CryptoJS locked with a passphrase.

scrypt skips the hasher, since RFC 7914 fixes it to SHA-256. argon2id, argon2i and argon2d skip it too and take their cost as an object, { memory, iterations, parallelism, keyLength }. bcrypt takes a 16-byte salt and a cost, and bcryptString turns its 23 bytes into the $2b$ string.

Need SHA-3, SHA-224 or SHA-384 under hmac or pbkdf2? Sha3_256Hasher, Sha3_512Hasher, Keccak256Hasher, Sha224Hasher and Sha384Hasher plug right in.

A hasher works alone too. Feed it with update, finish with digest. OpenPGP's S2K pushes up to 65 MB through one hash. Gluing that into one buffer first would be silly. So would create("sha224").hasher() in a bundle. It works, but it drags the whole registry along. About 100 kB for a hash that fits in 4 kB.

What if the sponge itself is in the way? STROBE pokes the raw Keccak state byte by byte. Merlin runs on STROBE, and sr25519 runs on Merlin. So keccakF1600 hands you the permutation bare. Give it 200 bytes, lanes little-endian. It permutes them in place. Same rounds SHA-3 runs, so nobody carries a second Keccak.

Import one and the registry stays out of your bundle. Pass a string and you get a HashError. A hex string hashed as text is exactly the bug from the section above, so here it's not an option at all.

That's a bundler talking. Plain Node doesn't tree-shake, so import { sha1 } from "@agntn/hashes" still parses every algorithm in the package, and a consumer that needs one digest pays for them on every start. Each file of byte functions has its own entry for that:

ts
import { sha1 } from "@agntn/hashes/sha1";
import { Sha256Hasher } from "@agntn/hashes/sha2";
import { hmac } from "@agntn/hashes/hmac";
EntryExports
@agntn/hashes/sha1sha1, Sha1Hasher
@agntn/hashes/md5md5, Md5Hasher
@agntn/hashes/sha2sha224, sha256, sha256Bits, sha384, sha512, hash256, Sha224Hasher, Sha256Hasher, Sha384Hasher, Sha512Hasher
@agntn/hashes/ripemd160ripemd160, hash160, Ripemd160Hasher
@agntn/hashes/keccakkeccak256, sha3_256, sha3_512, keccakF1600, Keccak256Hasher, Sha3_256Hasher, Sha3_512Hasher
@agntn/hashes/blake2bblake2b, Blake2bHasher
@agntn/hashes/blake256blake256
@agntn/hashes/crccrc32, crc64, crc24, crc16Xmodem, CRC32_VARIANTS, CRC64_VARIANTS, CRC24_VARIANTS
@agntn/hashes/adler32adler32
@agntn/hashes/xxhashxxhash, XXHASH_BITS
@agntn/hashes/hmachmac, pbkdf2, hkdf, hkdfExtract, hkdfExpand
@agntn/hashes/evpevpBytesToKey
@agntn/hashes/scryptscrypt
@agntn/hashes/argon2argon2id, argon2i, argon2d
@agntn/hashes/bcryptbcrypt, bcryptString

They're the same functions the root exports, not copies. On Node 26 sha1 from its own entry loads about three times faster than from the root.

Names

create() is strict: the exact registry key or an UnknownAlgorithmError. For names a human typed, use resolveAlgorithm(). It ignores case and punctuation and looks the result up among the keys and the labels, nothing smarter. A name that is a key wins. Otherwise, if two algorithms spell the same that way, it takes neither.

ts
import { algorithms, has, resolveAlgorithm } from "@agntn/hashes";

resolveAlgorithm("SHA3_256").name(); // "sha3-256"
resolveAlgorithm("Blake2b 256").name(); // "blake2b-256"
resolveAlgorithm("RIPEMD-160").name(); // "ripemd160"
has("sha3-384"); // false, unless you registered one
algorithms(); // every key, built-ins in listing order, then yours

No fuzzy match on purpose. sha3 could mean four different things, and guessing is how you get a wrong digest that looks right.

It won't guess, but it does point. Half a name gets the keys it begins, and a variant written into the name gets the option that takes it:

ts
resolveAlgorithm("sha3"); // UnknownAlgorithmError: ... Did you mean sha3-256 or sha3-512?
resolveAlgorithm("CRC-32/BZIP2"); // ... Did you mean crc32 with variant=bzip2?
resolveAlgorithm("tiger2"); // ... Available: sha256, sha384, ...

Why no hint for tiger2? Tiger is one letter away, and Tiger2 pads differently, so that hint would hand you the wrong digest with a straight face. error.suggestions carries the same hints for code.

What an algorithm says about itself

info() is how an algorithm describes itself, and the site, the CLI and the tools all read it:

ts
create("keccak256").info();
// {
//   name: "keccak256",
//   label: "Keccak-256",
//   family: "Keccak",
//   category: "cryptographic",
//   digestLength: 32,
//   hmac: true,
//   options: [{ name: "encoding", ... }, { name: "key", ... }],
//   securityNote: "128-bit collision resistance, ...",
//   ...
// }

options lists everything the algorithm takes, with type, default and description. A KDF declares salt and its costs there, xxHash its seed. The CLI turns them into flags and the tools take them as parameters, so an option you add in one place reaches all of them.

Families and categories

Two different questions. The family is where an algorithm comes from: SHA, Keccak, BLAKE, RIPEMD, MD, CRC, xxHash, FNV, scrypt, PBKDF, HKDF, OpenSSL, Argon2. SHA-0 and SHA3-256 are both SHA, and they have nothing else in common.

The category is what it's fit for. Four of them, and they mean what they say:

  • cryptographic. SHA-2, SHA-3, Keccak, BLAKE, RIPEMD-160, the Bitcoin compositions and HKDF. Use these when it matters.
  • legacy. MD5, SHA-1 and SHA-0. Broken for collisions, the first two still in every checksum file and inside Git. Fine for matching a checksum somebody else published. Not fine for anything you'd sign.
  • non-cryptographic. CRC-32, CRC-16/XMODEM, xxHash, FNV-1a. Fast, tiny, trivial to collide on purpose. Tables, dedup, error detection.
  • password. scrypt, PBKDF2 and the three Argon2 variants. Slow on purpose, and they need a salt. They have their own page. EVP_BytesToKey sits here too, fast and weak. It opens what openssl enc and CryptoJS locked.

hashes algorithms -c legacy lists one category, -f sha one family, and both together narrow it down to SHA-1 and SHA-0. hashes_algorithms takes category and family the same way. Case doesn't matter for the family.