Hmac.php 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. <?php
  2. namespace Libs;
  3. /**
  4. * Assinatura e verificação HMAC-SHA256.
  5. *
  6. * Usado para autenticar webhooks máquina-a-máquina: quem envia calcula
  7. * HMAC_SHA256(corpo, segredo) e manda no header; quem recebe recalcula com o
  8. * mesmo segredo e compara. Como o segredo nunca trafega, só quem o possui
  9. * consegue gerar uma assinatura válida — e qualquer alteração no corpo
  10. * invalida a assinatura.
  11. *
  12. * O segredo é compartilhado entre os dois lados por um canal seguro e, neste
  13. * projeto, é armazenado por empresa (company.company_hmac_secret).
  14. */
  15. final class Hmac
  16. {
  17. private const ALGO = 'sha256';
  18. /**
  19. * Prefixo opcional aceito no header de assinatura (ex.: "sha256=ab12...").
  20. * Muitos emissores usam esse formato; aceitamos com ou sem prefixo.
  21. */
  22. private const PREFIX = 'sha256=';
  23. /**
  24. * Gera um novo segredo HMAC forte (32 bytes -> 64 caracteres hexadecimais).
  25. * Deve ser usado ao provisionar a integração de CRM de uma empresa.
  26. */
  27. public static function generateSecret(): string
  28. {
  29. return bin2hex(random_bytes(32));
  30. }
  31. /**
  32. * Calcula a assinatura hexadecimal do corpo com o segredo informado.
  33. */
  34. public static function sign(string $body, string $secret): string
  35. {
  36. return hash_hmac(self::ALGO, $body, $secret);
  37. }
  38. /**
  39. * Verifica se a assinatura recebida corresponde ao corpo, usando o segredo.
  40. *
  41. * - Rejeita quando segredo ou assinatura estão vazios (config ausente).
  42. * - Aceita a assinatura com ou sem o prefixo "sha256=".
  43. * - Compara em tempo constante (hash_equals) para evitar timing attacks.
  44. */
  45. public static function verify(string $body, string $secret, string $providedSignature): bool
  46. {
  47. if ($secret === '' || $providedSignature === '') {
  48. return false;
  49. }
  50. $provided = $providedSignature;
  51. if (str_starts_with($provided, self::PREFIX)) {
  52. $provided = substr($provided, strlen(self::PREFIX));
  53. }
  54. $expected = self::sign($body, $secret);
  55. return hash_equals($expected, $provided);
  56. }
  57. }