Files
OmniRoute/open-sse/utils/sha3-512.ts
Diego Rodrigues de Sa e Souza 0adae00c7b Release v3.8.42 (#5459)
Release v3.8.42 — full CHANGELOG in CHANGELOG.md.

CI: 103 checks green incl. CodeQL (all languages), Semgrep, all 8 unit shards,
coverage, Node 24 compat, and integration tests. Full unit suite validated
locally: 19437 pass / 0 fail. The 3 red checks are advisory and do not gate
main (no required status checks): SonarCloud/SonarQube new-code coverage gate,
and PR Test Policy (test-masking detector flagging the legitimate dead-Phind
provider removal in #5530 — reviewed, correct).

Includes cycle-close reconciliation + repair of inherited base-red tests from
#5480/#5527/#5427/#5521 that the PR->release fast-path did not exercise.
2026-06-30 06:54:29 -03:00

165 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Runtime-portable SHA3-512 (FIPS-202).
//
// The ChatGPT-Web sentinel proof-of-work (open-sse/executors/chatgpt-web.ts)
// hashes with SHA3-512. Node built against OpenSSL has it natively, but the
// **Electron desktop app** ships Node built against **BoringSSL, which does not
// implement the SHA-3 family** (electron/electron#30530). There,
// `createHash("sha3-512")` throws `Error: Digest method not supported`, so every
// chatgpt-web request fails with `502 ChatGPT sentinel failed: Digest method not
// supported` and the provider is unusable on the desktop app (#5531).
//
// This module prefers the native digest (fast path on servers / OpenSSL) and
// transparently falls back to a dependency-free pure-JS Keccak-f[1600] when the
// runtime's crypto lacks SHA-3. The pure-JS path is validated bit-for-bit
// against the native digest and the published FIPS-202 vectors in the unit test.
import { createHash } from "node:crypto";
// ─── Keccak-f[1600] (BigInt lanes — correctness-first; only the fallback runs it) ──
const MASK = (1n << 64n) - 1n;
// Round constants RC[0..23].
const RC: bigint[] = [
0x0000000000000001n,
0x0000000000008082n,
0x800000000000808an,
0x8000000080008000n,
0x000000000000808bn,
0x0000000080000001n,
0x8000000080008081n,
0x8000000000008009n,
0x000000000000008an,
0x0000000000000088n,
0x0000000080008009n,
0x000000008000000an,
0x000000008000808bn,
0x800000000000008bn,
0x8000000000008089n,
0x8000000000008003n,
0x8000000000008002n,
0x8000000000000080n,
0x000000000000800an,
0x800000008000000an,
0x8000000080008081n,
0x8000000000008080n,
0x0000000080000001n,
0x8000000080008008n,
];
// Rotation offsets r[x+5y] (rho step).
const ROT: number[] = [
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
];
function rotl64(x: bigint, n: number): bigint {
if (n === 0) return x;
const bn = BigInt(n);
return ((x << bn) | (x >> (64n - bn))) & MASK;
}
function keccakF1600(s: bigint[]): void {
const C = new Array<bigint>(5);
const D = new Array<bigint>(5);
const B = new Array<bigint>(25);
for (let round = 0; round < 24; round++) {
// θ
for (let x = 0; x < 5; x++) C[x] = s[x] ^ s[x + 5] ^ s[x + 10] ^ s[x + 15] ^ s[x + 20];
for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1);
for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) s[x + 5 * y] ^= D[x];
// ρ + π
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(s[x + 5 * y], ROT[x + 5 * y]);
}
}
// χ
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
s[x + 5 * y] = B[x + 5 * y] ^ (~B[((x + 1) % 5) + 5 * y] & MASK & B[((x + 2) % 5) + 5 * y]);
}
}
// ι
s[0] ^= RC[round];
}
}
// SHA3-512: rate r = 576 bits (72 bytes / 9 lanes), capacity 1024, output 64 bytes.
const RATE_BYTES = 72;
function sha3_512Bytes(msg: Uint8Array): Uint8Array {
const s: bigint[] = new Array<bigint>(25).fill(0n);
// FIPS-202 pad10*1 with SHA-3 domain separation (first pad byte 0x06, last |= 0x80).
const padLen = RATE_BYTES - (msg.length % RATE_BYTES);
const padded = new Uint8Array(msg.length + padLen);
padded.set(msg);
padded[msg.length] = 0x06;
padded[padded.length - 1] |= 0x80;
// Absorb (little-endian lanes).
for (let off = 0; off < padded.length; off += RATE_BYTES) {
for (let i = 0; i < RATE_BYTES / 8; i++) {
let lane = 0n;
for (let b = 0; b < 8; b++) lane |= BigInt(padded[off + i * 8 + b]) << BigInt(8 * b);
s[i] ^= lane;
}
keccakF1600(s);
}
// Squeeze 64 bytes (8 lanes — fits in one rate block).
const out = new Uint8Array(64);
for (let i = 0; i < 8; i++) {
const lane = s[i];
for (let b = 0; b < 8; b++) out[i * 8 + b] = Number((lane >> BigInt(8 * b)) & 0xffn);
}
return out;
}
function toBytes(input: string | Uint8Array): Uint8Array {
return typeof input === "string" ? new Uint8Array(Buffer.from(input, "utf8")) : input;
}
/** Pure-JS SHA3-512 hex digest. Always runs the vendored Keccak (used as the fallback). */
export function sha3_512HexJs(input: string | Uint8Array): string {
return Buffer.from(sha3_512Bytes(toBytes(input))).toString("hex");
}
type NativeHasher = (data: Uint8Array) => string;
// undefined = not yet probed; null = native SHA-3 unavailable (BoringSSL/Electron).
let nativeHasher: NativeHasher | null | undefined;
function detectNative(): NativeHasher | null {
try {
// Construct + digest once — Electron/BoringSSL throws here, not lazily.
createHash("sha3-512").update(Buffer.alloc(0)).digest("hex");
return (data) => createHash("sha3-512").update(data).digest("hex");
} catch {
return null;
}
}
/**
* SHA3-512 hex digest that works on every runtime: native (OpenSSL) where
* available, pure-JS Keccak fallback where the crypto backend lacks SHA-3
* (Electron/BoringSSL — #5531). Capability is probed once and cached.
*/
export function sha3_512Hex(input: string | Uint8Array): string {
const data = toBytes(input);
if (nativeHasher === undefined) nativeHasher = detectNative();
if (nativeHasher) {
try {
return nativeHasher(data);
} catch {
nativeHasher = null; // became unavailable mid-flight — defensive
}
}
return sha3_512HexJs(data);
}
/** Test seam: force the native capability state (null = simulate BoringSSL, undefined = re-probe). */
export function __setSha3NativeForTesting(state: NativeHasher | null | undefined): void {
nativeHasher = state;
}