Segurança

Verificação e Segurança

A assinatura HMAC é obrigatória. Sem validá-la, qualquer um que descubra a URL do webhook pode forjar eventos de pagamento.
Header: X-HyzePay-Signature: t=<timestamp>,v1=<hex>. Use o raw body — nunca re-serialize o JSON.

Como calculamos

  1. Pegamos o timestamp Unix em segundos (t)
  2. Montamos a string: `${t}.${rawBody}`
    rawBody = corpo exato do POST (bytes UTF-8, sem re-serializar)
  3. signature = HMAC_SHA256(secret, signedPayload) em hex
  4. Header: X-HyzePay-Signature: t=<timestamp>,v1=<signature>

Como validar no seu servidor

  1. Leia o body cru (raw string)
  2. Extraia t e v1 do header
  3. Rejeite se |now - t| > 300 segundos (5 min) — evita replay
  4. Recalcule o HMAC com o mesmo secret do painel (whsec_…)
  5. Compare com timingSafeEqual (nunca === em produção se puder)

Node.js / TypeScript

import { createHmac, timingSafeEqual } from "crypto";

export function verifyHyzePaySignature(opts: {
  secret: string;
  rawBody: string;
  signatureHeader: string;
  toleranceSec?: number;
}): boolean {
  const tolerance = opts.toleranceSec ?? 300;
  const parts = opts.signatureHeader.split(",").map((p) => p.trim());
  let t: number | null = null;
  const signatures: string[] = [];

  for (const part of parts) {
    const [k, v] = part.split("=");
    if (k === "t" && v) t = Number(v);
    if (k === "v1" && v) signatures.push(v);
  }

  if (t == null || !Number.isFinite(t) || signatures.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > tolerance) return false;

  const expected = createHmac("sha256", opts.secret)
    .update(`${t}.${opts.rawBody}`, "utf8")
    .digest("hex");

  const expectedBuf = Buffer.from(expected, "utf8");
  return signatures.some((sig) => {
    try {
      const got = Buffer.from(sig, "utf8");
      return got.length === expectedBuf.length && timingSafeEqual(got, expectedBuf);
    } catch {
      return false;
    }
  });
}

Express (raw body)

import express from "express";

const app = express();

// Importante: capturar raw body ANTES do json parser global
app.post(
  "/webhooks/hyzepay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const sig = req.header("x-hyzepay-signature") || "";

    if (!verifyHyzePaySignature({
      secret: process.env.HYZEPAY_WEBHOOK_SECRET!,
      rawBody,
      signatureHeader: sig,
    })) {
      return res.status(401).json({ error: "invalid_signature" });
    }

    const event = JSON.parse(rawBody);
    // … processar …
    return res.status(200).json({ received: true });
  },
);

Next.js App Router

// app/api/webhooks/hyzepay/route.ts
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const rawBody = await req.text();
  const sig = req.headers.get("x-hyzepay-signature") || "";

  if (!verifyHyzePaySignature({
    secret: process.env.HYZEPAY_WEBHOOK_SECRET!,
    rawBody,
    signatureHeader: sig,
  })) {
    return NextResponse.json({ error: "invalid_signature" }, { status: 401 });
  }

  const event = JSON.parse(rawBody);
  // v2: event.type + event.data.object
  // v1: event.event + event.data

  return NextResponse.json({ received: true });
}

Python

import hmac, hashlib, time

def verify_hyzepay_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    t = None
    signatures = []
    for part in header.split(","):
        part = part.strip()
        if part.startswith("t="):
            t = int(part[2:])
        elif part.startswith("v1="):
            signatures.append(part[3:])
    if t is None or not signatures:
        return False
    if abs(int(time.time()) - t) > tolerance:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)

PHP

function verifyHyzePaySignature(string $secret, string $rawBody, string $header, int $tolerance = 300): bool {
    $t = null;
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        $part = trim($part);
        if (str_starts_with($part, 't=')) $t = (int) substr($part, 2);
        if (str_starts_with($part, 'v1=')) $signatures[] = substr($part, 3);
    }
    if ($t === null || count($signatures) === 0) return false;
    if (abs(time() - $t) > $tolerance) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($signatures as $sig) {
        if (hash_equals($expected, $sig)) return true;
    }
    return false;
}