Module: Mailkite

Defined in:
lib/mailkite.rb

Defined Under Namespace

Classes: Client, Error

Constant Summary collapse

VERSION =
"0.1.0"
DEFAULT_BASE_URL =
"https://api.mailkite.dev"
DEFAULT_TOLERANCE_MS =

Reject webhook events older than this (ms) to block replays. Pass 0 to disable.

5 * 60 * 1000

Class Method Summary collapse

Class Method Details

.secure_compare(a, b) ⇒ Object

Length-independent constant-time string compare (no openssl >= 2.2 needed).



48
49
50
51
52
53
54
# File 'lib/mailkite.rb', line 48

def self.secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  diff = 0
  a.bytes.each_with_index { |byte, i| diff |= byte ^ b.getbyte(i) }
  diff.zero?
end

.verify_webhook(signature, payload, secret, tolerance_ms = DEFAULT_TOLERANCE_MS) ⇒ Object

Verify an ‘x-mailkite-signature` header on an inbound webhook delivery. Local HMAC-SHA256 check — no network call. Pass the raw, unparsed body.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/mailkite.rb', line 23

def self.verify_webhook(signature, payload, secret, tolerance_ms = DEFAULT_TOLERANCE_MS)
  return false unless signature.is_a?(String) && !signature.empty?

  parts = {}
  signature.split(",").each do |seg|
    i = seg.index("=")
    next unless i
    parts[seg[0...i].strip] = seg[(i + 1)..].strip
  end
  t = parts["t"]
  v1 = parts["v1"]
  return false if t.nil? || v1.nil? || t.empty? || v1.empty? || !t.match?(/\A-?\d+\z/)

  # The t in the header is milliseconds since the epoch.
  if tolerance_ms && tolerance_ms > 0
    return false if ((Time.now.to_f * 1000) - t.to_i).abs > tolerance_ms
  end

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{payload}")
  secure_compare(expected, v1)
rescue StandardError
  false
end