Files
2026-07-13 13:39:12 +08:00

165 lines
5.5 KiB
TypeScript
Raw Permalink 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;
}