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/sha2and@noble/hashes/sha1.subtleHashBridge(algorithm, signal?)— async digest viaglobalThis.crypto.subtle.digest. Honours an optionalAbortSignalboth at construction and per-call.defaultHashBridge(algorithm)— picks subtle when available, falls back to noble.digestSync(hash, input)— internal helper that runs aHashLikeand throwsSRP.HASH_UNSUPPORTEDif 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.digestinvocation.
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).