Current section
Files
Jump to
Current section
Files
lib/captain_hook_client.ex
defmodule CaptainHookClient do
@moduledoc """
`CaptainHookClient`.
"""
@schema "v1"
@default_valid_period_in_seconds 300
@spec verify(binary, binary, binary, keyword) :: :ok | {:error, binary}
def verify(header, payload, secret, opts \\ []) do
with {:ok, timestamp, hashes} <- parse_signature_header(header, @schema) do
valid_period_in_seconds =
Keyword.get(opts, :valid_period_in_seconds, @default_valid_period_in_seconds)
current_timestamp = Keyword.get(opts, :system, System).system_time(:second)
expected_hash = hash(payload, timestamp, secret)
hashes
|> Enum.map(&Plug.Crypto.secure_compare(&1, expected_hash))
cond do
timestamp + valid_period_in_seconds < current_timestamp ->
{:error, "signature is too old"}
Enum.all?(hashes, &(Plug.Crypto.secure_compare(&1, expected_hash) == false)) ->
{:error, "signature is incorrect"}
true ->
:ok
end
end
end
defp hash(payload, timestamp, secret) do
:crypto.hmac(:sha256, secret, "#{timestamp}.#{payload}")
|> Base.encode16(case: :lower)
end
@spec parse_signature_header(binary, binary) ::
{:error, binary} | {:ok, timestamp :: integer, hashes :: [binary]}
defp parse_signature_header(signature, schema) do
parsed =
for pair <- String.split(signature, ","),
destructure([key, value], String.split(pair, "=", parts: 2)),
do: {key, value},
into: []
with [{"t", timestamp} | hashes] <- parsed,
{timestamp, ""} <- Integer.parse(timestamp),
{^schema, _} <- hashes |> List.first() do
hashes =
hashes
|> Enum.filter(fn {key, _value} -> key == schema end)
|> Enum.map(fn {_key, value} -> value end)
{:ok, timestamp, hashes}
else
_ -> {:error, "signature is in a wrong format or is missing v1 schema"}
end
end
end