# frozen_string_literal: true require 'openssl' require 'active_support/security_utils' module StrikeMe module API # Verifies the X-Webhook-Signature header Strike attaches to every # webhook POST — HMAC-SHA256 of the raw request body, keyed by the # secret set at webhook-subscription creation time. See # https://docs.strike.me/webhooks/signature-verification — this # mirrors Strike's own documented reference implementation exactly # (hex digest, timing-safe compare), just in Ruby. # # `raw_body` must be the exact bytes Strike sent, before any # JSON-parsing round-trip — re-serializing a parsed body can reorder # keys or change whitespace and silently break verification. module Webhook SIGNATURE_HEADER = 'X-Webhook-Signature' def self.valid_signature?(raw_body:, signature:, secret:) return false if signature.blank? || raw_body.blank? expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body) secure_compare(expected, signature) end # ActiveSupport::SecurityUtils.secure_compare raises on a length # mismatch rather than returning false — a garbled/truncated header # from a malformed request should fail closed, not raise. def self.secure_compare(expected, actual) return false unless expected.bytesize == actual.bytesize ActiveSupport::SecurityUtils.secure_compare(expected, actual) end end end end