import { sha1 } from "@noble/hashes/sha1.js"; import { sha256 } from "@noble/hashes/sha2.js"; import type { HashLike } from "@suaveplan/types"; import { SRP_CODES, srpError } from "../errors/errors.js"; /** Supported algorithm tags for `@suaveplan/srp`. */ export type SrpHashAlgorithm = "SHA256" | "SHA1"; const ALGORITHMS: readonly SrpHashAlgorithm[] = ["SHA256", "SHA1"] as const; function digestSizeFor(algorithm: SrpHashAlgorithm): number { return algorithm === "SHA256" ? 32 : 20; } function subtleNameFor(algorithm: SrpHashAlgorithm): string { return algorithm === "SHA256" ? "SHA-256" : "SHA-1"; } function assertSupported(algorithm: SrpHashAlgorithm): void { if (!ALGORITHMS.includes(algorithm)) { throw srpError( SRP_CODES.HASH_UNSUPPORTED, `unsupported algorithm: ${String(algorithm)}` ); } } /** Sync `@noble/hashes`-backed `HashLike`. SHA-256 or SHA-1. */ export function nobleHashBridge(algorithm: SrpHashAlgorithm): HashLike { assertSupported(algorithm); const digestSize = digestSizeFor(algorithm); return { algorithm, digestSize, digest(input: Uint8Array): Uint8Array { return algorithm === "SHA256" ? sha256(input) : sha1(input); }, }; } /** * Async `crypto.subtle.digest`-backed `HashLike`. Honours an optional * `AbortSignal` — a pre-aborted signal causes the digest to reject * before any work is performed; mid-flight aborts propagate to the * caller. * * `signal` MAY be supplied at construction time (applies to every * `digest` call) AND/OR per-call. If both are present, the per-call * signal takes precedence. */ export function subtleHashBridge( algorithm: SrpHashAlgorithm, constructionSignal?: AbortSignal ): HashLike { assertSupported(algorithm); const digestSize = digestSizeFor(algorithm); const subtleName = subtleNameFor(algorithm); return { algorithm, digestSize, async digest( input: Uint8Array, callSignal?: AbortSignal ): Promise { const signal = callSignal ?? constructionSignal; if (signal?.aborted) { throw signal.reason instanceof Error ? signal.reason : new Error("aborted"); } const subtle = globalThis.crypto?.subtle; if (subtle === undefined) { throw srpError( SRP_CODES.HASH_UNSUPPORTED, "globalThis.crypto.subtle is not available in this runtime" ); } // Copy into a fresh ArrayBuffer to satisfy WebCrypto's // BufferSource type (excludes SharedArrayBuffer in TS DOM lib). const copy = new Uint8Array(input.byteLength); copy.set(input); const buf = await subtle.digest(subtleName, copy.buffer); if (signal?.aborted) { throw signal.reason instanceof Error ? signal.reason : new Error("aborted"); } return new Uint8Array(buf); }, }; } /** * Pick the platform-appropriate backend. When `globalThis.crypto.subtle` * is available (browsers, Bun, Node >= 20), returns the async subtle * bridge. Otherwise falls back to the sync `@noble/hashes` bridge. */ export function defaultHashBridge(algorithm: SrpHashAlgorithm): HashLike { assertSupported(algorithm); if (typeof globalThis.crypto?.subtle?.digest === "function") { return subtleHashBridge(algorithm); } return nobleHashBridge(algorithm); } /** * Run a `HashLike.digest` and assert the result is synchronously * available. SRP-6a's handshake functions are CPU-bound and cannot * await an async digest mid-protocol; consumers that want async hashes * MUST drive the protocol externally and pass intermediates back in. * * Throws `SrpError(SRP.HASH_UNSUPPORTED)` if the returned digest is a * Promise — which is how a caller of the sync handshake API surface * accidentally passing `subtleHashBridge` discovers the mismatch. */ export function digestSync(hash: HashLike, input: Uint8Array): Uint8Array { const out = hash.digest(input); if (out instanceof Promise) { throw srpError( SRP_CODES.HASH_UNSUPPORTED, "async HashLike.digest used on a synchronous SRP path; pass a sync bridge (nobleHashBridge) or pre-resolve every digest before calling" ); } return out; }