Files
2026-05-27 23:47:53 +01:00

5.1 KiB

hash-bridge

(no submodules)

Purpose

Two HashLike implementations for @suaveplan/srp — nobleHashBridge (sync, @noble/hashes-backed) and subtleHashBridge (async, crypto.subtle.digest-backed) — plus a defaultHashBridge(algorithm) factory that picks the right one for the host runtime. Both backends produce byte-identical digests for the same input; the difference is whether the call returns a Uint8Array directly or a Promise<Uint8Array>. The package's SRP-6a handshake functions are CPU-bound and synchronous, so they require the sync noble bridge or a pre-resolved subtle output; digestSync is the gate that surfaces the mismatch.

Features

  • nobleHashBridge("SHA256" | "SHA1") — sync digest via @noble/hashes/sha2 and @noble/hashes/sha1.
  • subtleHashBridge(algorithm, signal?) — async digest via globalThis.crypto.subtle.digest. Honours an optional AbortSignal both at construction and per-call.
  • defaultHashBridge(algorithm) — picks subtle when available, falls back to noble.
  • digestSync(hash, input) — internal helper that runs a HashLike and throws SRP.HASH_UNSUPPORTED if the result is a Promise (used to gate sync handshake paths).
  • Unsupported algorithm → throws SrpError(SRP.HASH_UNSUPPORTED).
  • AbortSignal short-circuit: pre-aborted signals reject before any crypto.subtle.digest invocation.

Usage: Basic Example

import { nobleHashBridge, subtleHashBridge, defaultHashBridge } from "@suaveplan/srp/hash";

const noble = nobleHashBridge("SHA256");
const subtle = subtleHashBridge("SHA256");
const auto = defaultHashBridge("SHA256");

const input = new TextEncoder().encode("hello");
console.log(noble.digest(input).byteLength); // 32
console.log((await subtle.digest(input)).byteLength); // 32

Usage: Advanced Example

import { subtleHashBridge } from "@suaveplan/srp/hash";

const controller = new AbortController();
const hash = subtleHashBridge("SHA256");
const input = new Uint8Array(1024 * 1024);

setTimeout(() => controller.abort(new Error("deadline")), 10);
try {
    await hash.digest(input, controller.signal);
} catch (err) {
    console.log("aborted:", (err as Error).message);
}

API Reference

nobleHashBridge(algorithm: SrpHashAlgorithm): HashLike

Sync @noble/hashes backend. digest returns Uint8Array directly. SHA-256 → 32-byte output; SHA-1 → 20-byte output. Throws SrpError(SRP.HASH_UNSUPPORTED) on any other algorithm.

subtleHashBridge(algorithm: SrpHashAlgorithm, signal?: AbortSignal): HashLike

Async crypto.subtle.digest backend. digest returns Promise<Uint8Array>. The optional construction-time signal applies to every digest call; an optional per-call signal overrides it. Pre-aborted signals reject before invoking crypto.subtle.digest; mid-flight aborts propagate after the digest completes (crypto.subtle.digest does not natively support cancellation, but the post-digest re-check ensures the caller does not receive a stale result).

defaultHashBridge(algorithm: SrpHashAlgorithm): HashLike

Platform selector. Returns subtleHashBridge(algorithm) when globalThis.crypto.subtle.digest is callable, otherwise returns nobleHashBridge(algorithm). This factory captures the platform decision once so universal consumers do not branch at every call site.

digestSync(hash: HashLike, input: Uint8Array): Uint8Array

Sync gate: calls hash.digest(input) and asserts the result is a Uint8Array. Throws SrpError(SRP.HASH_UNSUPPORTED) if the impl returns a Promise. Every SRP handshake path in this package uses digestSync so the protocol cannot accidentally consume an async backend mid-step.

type SrpHashAlgorithm = "SHA256" | "SHA1"

Stable algorithm tags supported by this package.

Implementation Notes

The async-only subtleHashBridge exists for two scenarios: (1) browser bundles that already ship crypto.subtle and want to avoid pulling @noble/hashes into the bundle, and (2) deployments that want hardware-backed digests routed through the runtime's native subtle implementation. For the SRP-6a handshake itself, however, the sync noble bridge is mandatory because the protocol's H(H(N) XOR H(g) | H(I) | s | A | B | K) chain interleaves multiple digests with modPow calls — driving the whole chain through async digests would require restructuring the handshake API around Promises, which the spec explicitly avoids.

digestSync's throw rather than implicit-await design is deliberate: the package would otherwise leak Promise-based bugs (forgetting to await one digest in the chain would produce nonsense bytes downstream) into silent handshake failures rather than loud, debuggable errors. The throw at the bridge boundary stops the protocol immediately with a clear code.

AbortSignal handling in subtleHashBridge checks the signal both before and after the crypto.subtle.digest call. The pre-check ensures a pre-aborted signal never reaches the runtime; the post-check protects the caller from receiving a stale digest when the abort fires while the digest was in flight (the runtime may or may not propagate the cancellation, so the post-check is the safety net).