Assinaturas HMAC

Como confirmar que o pedido veio mesmo do DroidSender e não de outra pessoa.

CabeçalhoO que traz
X-DroidSender-Evento nome do evento, o mesmo que vai no corpo
X-DroidSender-Deliveryid único desta tentativa de entrega; deduplica por ele
X-DroidSender-Signaturea assinatura, descrita abaixo
User-AgentDroidSender-Webhooks/2.0

Todos os pedidos levam o cabeçalho X-DroidSender-Signature na forma t=<segundos unix>,v1=<hex>, onde v1 é o HMAC-SHA256 de "<t>.<corpo em bruto>" com o segredo do teu webhook.

const crypto = require("crypto");

function assinaturaValida(corpoBruto, cabecalho, segredo) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(cabecalho ?? "");
  if (!m) return false;

  const [, t, recebida] = m;

  // o carimbo faz parte do que é assinado: uma entrega intercetada nao
  // se reenvia amanha, e o t nao se reescreve sem o segredo
  const idade = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
  if (idade > 300) return false;

  const esperada = crypto
    .createHmac("sha256", segredo)
    .update(`${t}.${corpoBruto}`)
    .digest("hex");

  // comparacao em tempo constante: um === simples deixa medir o segredo
  return crypto.timingSafeEqual(
    Buffer.from(esperada, "hex"),
    Buffer.from(recebida, "hex"),
  );
}

Recusa o que tiver mais de cinco minutos

Sem a verificação da idade, um pedido reenviado continua a validar. Cinco minutos é a janela recomendada.

Usa o corpo em bruto

A assinatura é calculada sobre o JSON tal como chegou. Se o verificares depois de o converter em objeto e voltar a serializar, os espaços mudam e a assinatura nunca bate certo.