39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
const toHex = (buffer: ArrayBuffer): string =>
|
|
Array.from(new Uint8Array(buffer))
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
export const timingSafeEqualHex = (a: string, b: string): boolean => {
|
|
if (a.length !== b.length) return false;
|
|
let mismatch = 0;
|
|
for (let i = 0; i < a.length; i += 1) {
|
|
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
}
|
|
return mismatch === 0;
|
|
};
|
|
|
|
export const verifyGithubSignature = async (
|
|
secret: string,
|
|
rawBody: string,
|
|
signatureHeader: string | null,
|
|
): Promise<boolean> => {
|
|
if (!signatureHeader?.startsWith('sha256=')) return false;
|
|
// Web Crypto's importKey throws on a zero-length HMAC key; guard so the
|
|
// verifier never throws on attacker-controlled input (returns false instead).
|
|
if (!secret) return false;
|
|
const provided = signatureHeader.slice('sha256='.length);
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(secret),
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign'],
|
|
);
|
|
const mac = await crypto.subtle.sign(
|
|
'HMAC',
|
|
key,
|
|
new TextEncoder().encode(rawBody),
|
|
);
|
|
return timingSafeEqualHex(toHex(mac), provided);
|
|
};
|