Guide

Custom algorithms

Extend FixedHash or BlockHash or Hash and register the class. The CLI and the tools see it too

Three base classes

Every built-in is a class with a static key, and yours is the same shape. Pick the base by what you need:

  • FixedHash for a fixed-length digest. You write digest(bytes), it does the encodings, the result object, the error wrapping, and refuses an HMAC key.
  • BlockHash for a fixed-length digest built on an incremental Hasher. You write hasher(), and HMAC over its blocks comes for free.
  • Hash for anything else, like a KDF with its own options. You write info() and hash() yourself.

A fixed digest

FNV-1a in 32 bits, plain TypeScript, so it runs anywhere the library does:

tsfnv1a-32.ts
import { FixedHash, create, register } from "@agntn/hashes";

class Fnv1a32 extends FixedHash {
  static readonly key = "fnv1a-32";
  protected readonly about = {
    label: "FNV-1a (32-bit)",
    description: "FNV-1a 32-bit, for hash tables",
    family: "non-cryptographic",
    digestLength: 4,
  } as const;

  protected digest(bytes: Uint8Array): Uint8Array {
    let hash = 0x811c9dc5;
    for (const byte of bytes) hash = Math.imul(hash ^ byte, 0x01000193);
    const out = new Uint8Array(4);
    new DataView(out.buffer).setUint32(0, hash >>> 0);
    return out;
  }
}

register(Fnv1a32);
create("fnv1a-32").hash("a").digest; // "e40c292c"
create("fnv1a-32").hash("a", { key: "k" }); // throws, fnv1a-32 has no HMAC mode

about is everything info() reports besides the name, the options and HMAC support. Add securityNote if there's something a user should know before trusting it.

Borrowing from node:crypto

On Node you can wrap what node:crypto already has. The library itself doesn't import node:*, your file can:

tssha224.ts
import { createHash } from "node:crypto";
import { FixedHash, register } from "@agntn/hashes";

class Sha224 extends FixedHash {
  static readonly key = "sha224";
  protected readonly about = {
    label: "SHA-224",
    description: "SHA-2 family 224-bit hash",
    family: "cryptographic",
    digestLength: 28,
  } as const;

  protected digest(bytes: Uint8Array): Uint8Array {
    return createHash("sha224").update(bytes).digest();
  }
}

register(Sha224);

That one won't run in a browser, of course. The built-ins do, which is how this site works.

Registering

register(Class) puts the class under its key, replacing any algorithm with the same key and dropping its cached instance. After that create, has, algorithms, resolveAlgorithm and every tool executor see it. Importing the package registers nothing by itself. The built-ins seed the registry on first use, so importing mutates no shared state.

Options of your own

An option besides encoding and key goes in info().options, like xxHash's seed. That one declaration is what the tool executors check parameters against, so parameters: { seed } reaches your digest and a name you didn't declare is refused. The built-ins' options become CLI flags the same way. FixedHash takes them as a protected options array and passes the values into digest(bytes, options).