import { createHmac, randomBytes } from "node:crypto"; import type { NextcloudTalkWebhookHeaders } from "./types.js"; const SIGNATURE_HEADER = "x-nextcloud-talk-signature"; const RANDOM_HEADER = "x-nextcloud-talk-random"; const BACKEND_HEADER = "x-nextcloud-talk-backend"; /** * Verify the HMAC-SHA256 signature of an incoming webhook request. * Signature is calculated as: HMAC-SHA256(random + body, secret) */ export function verifyNextcloudTalkSignature(params: { signature: string; random: string; body: string; secret: string; }): boolean { const { signature, random, body, secret } = params; if (!signature || !random || !secret) return false; const expected = createHmac("sha256", secret) .update(random + body) .digest("hex"); if (signature.length !== expected.length) return false; let result = 0; for (let i = 0; i < signature.length; i++) { result |= signature.charCodeAt(i) ^ expected.charCodeAt(i); } return result === 0; } /** * Extract webhook headers from an incoming request. */ export function extractNextcloudTalkHeaders( headers: Record, ): NextcloudTalkWebhookHeaders | null { const getHeader = (name: string): string | undefined => { const value = headers[name] ?? headers[name.toLowerCase()]; return Array.isArray(value) ? value[0] : value; }; const signature = getHeader(SIGNATURE_HEADER); const random = getHeader(RANDOM_HEADER); const backend = getHeader(BACKEND_HEADER); if (!signature || !random || !backend) return null; return { signature, random, backend }; } /** * Generate signature headers for an outbound request to Nextcloud Talk. */ export function generateNextcloudTalkSignature(params: { body: string; secret: string }): { random: string; signature: string; } { const { body, secret } = params; const random = randomBytes(32).toString("hex"); const signature = createHmac("sha256", secret) .update(random + body) .digest("hex"); return { random, signature }; }