55 lines
1.3 KiB
TypeScript
55 lines
1.3 KiB
TypeScript
import crypto from "node:crypto";
|
|
|
|
const CAPTCHA_TTL_MS = 10 * 60 * 1000;
|
|
|
|
function getSecret() {
|
|
return process.env.CONTACT_CAPTCHA_SECRET ?? "fallback-contact-captcha-secret";
|
|
}
|
|
|
|
export function generateCaptchaChallenge() {
|
|
const left = crypto.randomInt(1, 10);
|
|
const right = crypto.randomInt(1, 10);
|
|
const answer = left + right;
|
|
const expiresAt = Date.now() + CAPTCHA_TTL_MS;
|
|
const payload = `${answer}:${expiresAt}`;
|
|
const signature = crypto
|
|
.createHmac("sha256", getSecret())
|
|
.update(payload)
|
|
.digest("hex");
|
|
|
|
return {
|
|
prompt: `${left} + ${right} = ?`,
|
|
token: `${payload}:${signature}`
|
|
};
|
|
}
|
|
|
|
export function verifyCaptchaToken(token: string, answer: string) {
|
|
const parts = token.split(":");
|
|
if (parts.length !== 3) {
|
|
return false;
|
|
}
|
|
|
|
const [expectedAnswer, expiresAt, signature] = parts;
|
|
const payload = `${expectedAnswer}:${expiresAt}`;
|
|
const expectedSignature = crypto
|
|
.createHmac("sha256", getSecret())
|
|
.update(payload)
|
|
.digest("hex");
|
|
|
|
const isSignatureValid = crypto.timingSafeEqual(
|
|
Buffer.from(signature),
|
|
Buffer.from(expectedSignature)
|
|
);
|
|
|
|
if (!isSignatureValid) {
|
|
return false;
|
|
}
|
|
|
|
if (Number(expiresAt) < Date.now()) {
|
|
return false;
|
|
}
|
|
|
|
return expectedAnswer === answer.trim();
|
|
}
|
|
|