mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
/**
|
|||
|
|
* One-tap email links: `GET /log?p=704&r=pass&d=2026-08-31&sig=<hmac>`.
|
||
|
|
* The sig is HMAC-SHA256 over `p|r|d` with LINK_KEY (WebCrypto, hex).
|
||
|
|
* Verification is constant-time; links older than 3 days are rejected.
|
||
|
|
*/
|
||
|
|
|
||
|
|
async function hmacKey(secret: string): Promise<CryptoKey> {
|
||
|
|
return crypto.subtle.importKey(
|
||
|
|
"raw",
|
||
|
|
new TextEncoder().encode(secret),
|
||
|
|
{ name: "HMAC", hash: "SHA-256" },
|
||
|
|
false,
|
||
|
|
["sign", "verify"],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function signLink(
|
||
|
|
key: string,
|
||
|
|
p: number,
|
||
|
|
r: string,
|
||
|
|
d: string,
|
||
|
|
): Promise<string> {
|
||
|
|
const mac = await crypto.subtle.sign(
|
||
|
|
"HMAC",
|
||
|
|
await hmacKey(key),
|
||
|
|
new TextEncoder().encode(`${p}|${r}|${d}`),
|
||
|
|
);
|
||
|
|
return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function verifyLink(
|
||
|
|
key: string,
|
||
|
|
p: number,
|
||
|
|
r: string,
|
||
|
|
d: string,
|
||
|
|
sig: string,
|
||
|
|
): Promise<boolean> {
|
||
|
|
if (!/^[0-9a-f]{64}$/.test(sig)) return false;
|
||
|
|
const bytes = new Uint8Array(32);
|
||
|
|
for (let i = 0; i < 32; i++) bytes[i] = Number.parseInt(sig.slice(i * 2, i * 2 + 2), 16);
|
||
|
|
// crypto.subtle.verify is constant-time; never compare hex strings directly.
|
||
|
|
return crypto.subtle.verify(
|
||
|
|
"HMAC",
|
||
|
|
await hmacKey(key),
|
||
|
|
bytes,
|
||
|
|
new TextEncoder().encode(`${p}|${r}|${d}`),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Constant-time equality for webhook signatures and admin bearer keys. */
|
||
|
|
export async function timingSafeEqual(a: string, b: string): Promise<boolean> {
|
||
|
|
// HMAC both sides with a random key: unequal-length inputs and content
|
||
|
|
// differences are equally invisible to timing.
|
||
|
|
const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, false, [
|
||
|
|
"sign",
|
||
|
|
]);
|
||
|
|
const enc = new TextEncoder();
|
||
|
|
const [ma, mb] = await Promise.all([
|
||
|
|
crypto.subtle.sign("HMAC", key, enc.encode(a)),
|
||
|
|
crypto.subtle.sign("HMAC", key, enc.encode(b)),
|
||
|
|
]);
|
||
|
|
const va = new Uint8Array(ma);
|
||
|
|
const vb = new Uint8Array(mb);
|
||
|
|
let diff = 0;
|
||
|
|
for (let i = 0; i < va.length; i++) diff |= va[i]! ^ vb[i]!;
|
||
|
|
return diff === 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** GitHub webhook `X-Hub-Signature-256: sha256=<hex>` verification. */
|
||
|
|
export async function verifyWebhook(
|
||
|
|
secret: string,
|
||
|
|
body: ArrayBuffer,
|
||
|
|
header: string | null,
|
||
|
|
): Promise<boolean> {
|
||
|
|
if (!header?.startsWith("sha256=")) return false;
|
||
|
|
const mac = await crypto.subtle.sign("HMAC", await hmacKey(secret), body);
|
||
|
|
const expected = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||
|
|
return timingSafeEqual(header.slice(7), expected);
|
||
|
|
}
|