Initial Commit
This commit is contained in:
125
packages/security-stack/security/srp/src/hash/hash-bridge.ts
Normal file
125
packages/security-stack/security/srp/src/hash/hash-bridge.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user