lib/scf/auth.ex

defmodule SCF.Auth do
  @moduledoc """
  TC3-HMAC-SHA256 Signature Algorithm

  [Signature V3](https://cloud.tencent.com/document/product/583/33846) with POST only.
  """

  @requested_service "scf"
  @termination_string "tc3_request"

  @spec authorization(encoded_body :: binary, headers :: [tuple], config :: map) :: binary
  def authorization(encoded_body, headers, config) do
    http_request_method = "POST"
    canonical_uri = "/"
    canonical_query_string = ""
    algorithm = "TC3-HMAC-SHA256"
    secret_id = Map.fetch!(config, :secret_id)
    secret_key = Map.fetch!(config, :secret_key)

    # 1. Concatenating the CanonicalRequest String
    formatted_headers = downcase_and_sort(headers)

    canonical_headers =
      formatted_headers
      |> Enum.map(fn {key, value} -> "#{key}:#{value}\n" end)

    signed_headers = formatted_headers |> Enum.map(fn {key, _value} -> key end) |> Enum.join(";")

    hashed_request_payload = encoded_body |> hash_sha256() |> Base.encode16(case: :lower)

    canonical_request =
      Enum.join(
        [
          http_request_method,
          canonical_uri,
          canonical_query_string,
          canonical_headers,
          signed_headers,
          hashed_request_payload
        ],
        "\n"
      )

    # 2. Concatenating the String to Be Signed
    {_key, request_timestamp} =
      Enum.find(formatted_headers, fn
        {"x-tc-timestamp", _value} -> true
        _ -> false
      end)

    credential_date =
      request_timestamp |> DateTime.from_unix!() |> DateTime.to_date() |> Date.to_iso8601()

    credential_scope = "#{credential_date}/#{@requested_service}/#{@termination_string}"
    hashed_canonical_request = canonical_request |> hash_sha256() |> Base.encode16(case: :lower)

    string_to_sign =
      Enum.join([algorithm, request_timestamp, credential_scope, hashed_canonical_request], "\n")

    # 3. Calculating the Signature
    signature =
      ("TC3" <> secret_key)
      |> hmac_sha256(credential_date)
      |> hmac_sha256(@requested_service)
      |> hmac_sha256(@termination_string)
      |> hmac_sha256(string_to_sign)
      |> Base.encode16(case: :lower)

    # 4. Concatenating the Authorization
    "#{algorithm} Credential=#{secret_id}/#{credential_scope}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
  end

  defp downcase_and_sort(headers) do
    headers
    |> Enum.map(fn
      {key, value} when is_binary(value) ->
        {String.downcase(key), value |> String.downcase() |> String.trim()}

      {key, value} ->
        {String.downcase(key), value}
    end)
    |> Enum.sort_by(fn {key, _value} -> key end)
  end

  defp hash_sha256(data), do: :crypto.hash(:sha256, data)
  defp hmac_sha256(key, data), do: SCF.Utils.hmac(:sha256, key, data)
end