Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
# `hash-bridge`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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).