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