126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
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<Uint8Array> {
|
|
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;
|
|
}
|