---
title: "Custom algorithms"
description: "Extend FixedHash or BlockHash or Kdf and register the class. The CLI and the tools see it too"
canonical_url: "https://hashes.agntn.dev/guide/custom"
---
# Custom algorithms

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

## Four 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.
- **Kdf** for a key derivation function. You write `derive(bytes)`, it does what `FixedHash` does and refuses `rounds` too.
- **Hash** for anything else. You write `info()` and `hash()` yourself, lists included.

## A fixed digest

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

```ts [fnv1a-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: "FNV",
    category: "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:

```ts [sha3-384.ts]
import { createHash } from "node:crypto";
import { FixedHash, register } from "@agntn/hashes";

class Sha3_384 extends FixedHash {
  static readonly key = "sha3-384";
  protected readonly about = {
    label: "SHA3-384",
    description: "SHA-3 family 384-bit hash",
    family: "SHA",
    category: "cryptographic",
    digestLength: 48,
  } as const;

  protected digest(bytes: Uint8Array): Uint8Array {
    return createHash("sha3-384").update(bytes).digest();
  }
}

register(Sha3_384);
```

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

## Your own KDF

Need a KDF the registry doesn't have? WPA2 turns a Wi-Fi passphrase into its key with PBKDF2-HMAC-SHA1. That's 4096 iterations, with the network name as salt. The built-in `pbkdf2` refuses SHA-1 on purpose. Yours doesn't have to:

```ts [wpa-psk.ts]
import { Kdf, MissingOptionError, Sha1Hasher, create, pbkdf2, register } from "@agntn/hashes";
import type { Derivation, HashOptions } from "@agntn/hashes";

interface WpaPskOptions extends HashOptions {
  ssid?: string;
}

class WpaPsk extends Kdf<WpaPskOptions> {
  static readonly key = "wpa-psk";
  protected readonly about = {
    label: "WPA-PSK",
    description: "WPA2 pre-shared key from a passphrase and the network name",
    family: "PBKDF",
    category: "password",
    digestLength: 32,
  } as const;
  protected override readonly options = [
    { name: "ssid", type: "string", required: true, description: "Network name" },
  ] as const;

  protected derive(passphrase: Uint8Array, options?: Readonly<WpaPskOptions>): Derivation {
    if (options?.ssid === undefined) throw new MissingOptionError("ssid");
    const salt = new TextEncoder().encode(options.ssid);
    return {
      digest: pbkdf2(() => new Sha1Hasher(), passphrase, salt, 4096, 32),
      reported: { ssid: options.ssid },
    };
  }
}

register(WpaPsk);
create("wpa-psk").hash("password", { ssid: "IEEE" } as WpaPskOptions).digest;
// "f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e"
```

`derive` gets the passphrase as bytes. It hands back the key and whatever a caller needs to derive it again. Lists, encodings, the result object and the error wrapping are on `Kdf`. So is saying no to an HMAC key or `rounds`. Your own parameters set the cost. Rename the network and the key changes with it. That's the salt doing its job.

Got a `keyLength` and a cost that hurts? Set `floorCosts` too, the options at which `derive` is nearly free and the key keeps its length. PBKDF2's are `{ iterations: 1 }`. When an expected digest has the wrong length, verify derives once at those costs to check the `keyLength` it's about to suggest. Leave them out and that check pays your caller's costs.

## 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)`. `Kdf` hands them to `derive`. If your algorithm draws an option at random when it's missing, the way a KDF draws its salt, mark it `random: true`. Verify then asks for the value the digest was made with, since a fresh one can't match.


## Sitemap

See the full [sitemap](https://hashes.agntn.dev/sitemap.md) for all pages.
