Skip to main content

src/aws@internal@sigv4.erl

-module(aws@internal@sigv4).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/sigv4.gleam").
-export([make_credentials/3, canonical_request/3, string_to_sign/4, signing_key/4, signature/2, authorization_header/6, sign/3, presigned_url/5]).
-export_type([signing_options/0, signing_credentials/0, canonical_parts/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-type signing_options() :: {signing_options,
        binary(),
        binary(),
        binary(),
        boolean(),
        boolean(),
        boolean()}.

-type signing_credentials() :: {signing_credentials,
        binary(),
        binary(),
        gleam@option:option(binary())}.

-type canonical_parts() :: {canonical_parts,
        binary(),
        binary(),
        binary(),
        list(aws@internal@http_request:header())}.

-file("src/aws/internal/sigv4.gleam", 39).
?DOC(
    " Convenience constructor mirroring the most common case: static keys\n"
    " with no session token.\n"
).
-spec make_credentials(binary(), binary(), gleam@option:option(binary())) -> signing_credentials().
make_credentials(Access_key_id, Secret_access_key, Session_token) ->
    {signing_credentials, Access_key_id, Secret_access_key, Session_token}.

-file("src/aws/internal/sigv4.gleam", 314).
?DOC(
    " Assemble the canonical-request line block both header-auth and\n"
    " query-auth need. The five `\\n`-joined parts are identical\n"
    " between the two flows; only the inputs differ (header-auth\n"
    " signs in-headers, query-auth signs an auth-augmented query\n"
    " string).\n"
).
-spec build_creq(binary(), binary(), binary(), binary(), binary(), binary()) -> binary().
build_creq(
    Method,
    Canonical_uri,
    Canonical_query,
    Canonical_headers_block,
    Signed_headers_list,
    Payload_hash
) ->
    <<<<<<<<<<<<<<<<<<<<Method/binary, "\n"/utf8>>/binary,
                                        Canonical_uri/binary>>/binary,
                                    "\n"/utf8>>/binary,
                                Canonical_query/binary>>/binary,
                            "\n"/utf8>>/binary,
                        Canonical_headers_block/binary>>/binary,
                    "\n"/utf8>>/binary,
                Signed_headers_list/binary>>/binary,
            "\n"/utf8>>/binary,
        Payload_hash/binary>>.

-file("src/aws/internal/sigv4.gleam", 384).
-spec headers_for_signing(
    list(aws@internal@http_request:header()),
    signing_credentials(),
    signing_options()
) -> list(aws@internal@http_request:header()).
headers_for_signing(Prepared, Creds, Opts) ->
    case {erlang:element(4, Creds), erlang:element(7, Opts)} of
        {{some, _}, true} ->
            gleam@list:filter(
                Prepared,
                fun(H) ->
                    string:lowercase(erlang:element(2, H)) /= <<"x-amz-security-token"/utf8>>
                end
            );

        {_, _} ->
            Prepared
    end.

-file("src/aws/internal/sigv4.gleam", 398).
-spec upsert_header(
    list(aws@internal@http_request:header()),
    binary(),
    binary(),
    boolean()
) -> list(aws@internal@http_request:header()).
upsert_header(Headers, Name, Value, Replace) ->
    Lower = string:lowercase(Name),
    Already_present = gleam@list:any(
        Headers,
        fun(H) -> string:lowercase(erlang:element(2, H)) =:= Lower end
    ),
    case {Already_present, Replace} of
        {true, true} ->
            gleam@list:map(
                Headers,
                fun(H@1) ->
                    case string:lowercase(erlang:element(2, H@1)) =:= Lower of
                        true ->
                            {header, erlang:element(2, H@1), Value};

                        false ->
                            H@1
                    end
                end
            );

        {true, false} ->
            Headers;

        {false, _} ->
            lists:append(Headers, [{header, Name, Value}])
    end.

-file("src/aws/internal/sigv4.gleam", 359).
-spec prepare_headers(
    aws@internal@http_request:http_request(),
    signing_credentials(),
    signing_options(),
    binary()
) -> list(aws@internal@http_request:header()).
prepare_headers(Req, Creds, Opts, Payload_hash) ->
    With_date = upsert_header(
        erlang:element(5, Req),
        <<"X-Amz-Date"/utf8>>,
        erlang:element(2, Opts),
        true
    ),
    With_body = case erlang:element(6, Opts) of
        true ->
            upsert_header(
                With_date,
                <<"X-Amz-Content-Sha256"/utf8>>,
                Payload_hash,
                true
            );

        false ->
            With_date
    end,
    case {erlang:element(4, Creds), erlang:element(7, Opts)} of
        {{some, Token}, false} ->
            upsert_header(
                With_body,
                <<"X-Amz-Security-Token"/utf8>>,
                Token,
                true
            );

        {_, _} ->
            With_body
    end.

-file("src/aws/internal/sigv4.gleam", 60).
-spec canonical_request(
    aws@internal@http_request:http_request(),
    signing_credentials(),
    signing_options()
) -> canonical_parts().
canonical_request(Req, Creds, Opts) ->
    Payload_hash = case erlang:element(6, Opts) of
        true ->
            aws_ffi:hex_encode(aws_ffi:sha256(erlang:element(6, Req)));

        false ->
            aws_ffi:hex_encode(
                aws_ffi:sha256(gleam_stdlib:identity(<<""/utf8>>))
            )
    end,
    Prepared = prepare_headers(Req, Creds, Opts, Payload_hash),
    Signing_headers = headers_for_signing(Prepared, Creds, Opts),
    Canonical_headers_block = aws@internal@sigv4_canonical:canonical_headers(
        Signing_headers
    ),
    Signed_headers_list = aws@internal@sigv4_canonical:signed_headers(
        Signing_headers
    ),
    Canonical_uri = aws@internal@sigv4_canonical:build_canonical_uri(
        erlang:element(3, Req),
        erlang:element(5, Opts)
    ),
    Canonical_query = aws@internal@sigv4_canonical:canonical_query_string(
        erlang:element(4, Req)
    ),
    Creq = build_creq(
        erlang:element(2, Req),
        Canonical_uri,
        Canonical_query,
        Canonical_headers_block,
        Signed_headers_list,
        Payload_hash
    ),
    {canonical_parts, Creq, Signed_headers_list, Payload_hash, Prepared}.

-file("src/aws/internal/sigv4.gleam", 97).
-spec string_to_sign(binary(), binary(), binary(), binary()) -> binary().
string_to_sign(Canonical, Timestamp, Region, Service) ->
    Date = gleam@string:slice(Timestamp, 0, 8),
    Scope = <<<<<<<<<<Date/binary, "/"/utf8>>/binary, Region/binary>>/binary,
                "/"/utf8>>/binary,
            Service/binary>>/binary,
        "/aws4_request"/utf8>>,
    Hash = aws_ffi:hex_encode(aws_ffi:sha256(gleam_stdlib:identity(Canonical))),
    <<<<<<<<<<"AWS4-HMAC-SHA256\n"/utf8, Timestamp/binary>>/binary, "\n"/utf8>>/binary,
                Scope/binary>>/binary,
            "\n"/utf8>>/binary,
        Hash/binary>>.

-file("src/aws/internal/sigv4.gleam", 109).
-spec signing_key(binary(), binary(), binary(), binary()) -> bitstring().
signing_key(Secret, Date, Region, Service) ->
    K_secret = gleam_stdlib:identity(<<"AWS4"/utf8, Secret/binary>>),
    K_date = aws_ffi:hmac_sha256(K_secret, gleam_stdlib:identity(Date)),
    K_region = aws_ffi:hmac_sha256(K_date, gleam_stdlib:identity(Region)),
    K_service = aws_ffi:hmac_sha256(K_region, gleam_stdlib:identity(Service)),
    aws_ffi:hmac_sha256(
        K_service,
        gleam_stdlib:identity(<<"aws4_request"/utf8>>)
    ).

-file("src/aws/internal/sigv4.gleam", 122).
-spec signature(bitstring(), binary()) -> binary().
signature(Key, Sts) ->
    aws_ffi:hex_encode(aws_ffi:hmac_sha256(Key, gleam_stdlib:identity(Sts))).

-file("src/aws/internal/sigv4.gleam", 126).
-spec authorization_header(
    signing_credentials(),
    binary(),
    binary(),
    binary(),
    binary(),
    binary()
) -> binary().
authorization_header(
    Creds,
    Timestamp,
    Region,
    Service,
    Signed_headers,
    Signature
) ->
    Date = gleam@string:slice(Timestamp, 0, 8),
    <<<<<<<<<<<<<<<<<<<<<<"AWS4-HMAC-SHA256 Credential="/utf8,
                                                (erlang:element(2, Creds))/binary>>/binary,
                                            "/"/utf8>>/binary,
                                        Date/binary>>/binary,
                                    "/"/utf8>>/binary,
                                Region/binary>>/binary,
                            "/"/utf8>>/binary,
                        Service/binary>>/binary,
                    "/aws4_request, SignedHeaders="/utf8>>/binary,
                Signed_headers/binary>>/binary,
            ", Signature="/utf8>>/binary,
        Signature/binary>>.

-file("src/aws/internal/sigv4.gleam", 149).
-spec sign(
    aws@internal@http_request:http_request(),
    signing_credentials(),
    signing_options()
) -> aws@internal@http_request:http_request().
sign(Req, Creds, Opts) ->
    Parts = canonical_request(Req, Creds, Opts),
    Sts = string_to_sign(
        erlang:element(2, Parts),
        erlang:element(2, Opts),
        erlang:element(3, Opts),
        erlang:element(4, Opts)
    ),
    Date = gleam@string:slice(erlang:element(2, Opts), 0, 8),
    Key = signing_key(
        erlang:element(3, Creds),
        Date,
        erlang:element(3, Opts),
        erlang:element(4, Opts)
    ),
    Sig = signature(Key, Sts),
    Auth = authorization_header(
        Creds,
        erlang:element(2, Opts),
        erlang:element(3, Opts),
        erlang:element(4, Opts),
        erlang:element(3, Parts),
        Sig
    ),
    With_session = case {erlang:element(4, Creds), erlang:element(7, Opts)} of
        {{some, Token}, true} ->
            lists:append(
                erlang:element(5, Parts),
                [{header, <<"X-Amz-Security-Token"/utf8>>, Token}]
            );

        {_, _} ->
            erlang:element(5, Parts)
    end,
    Final_headers = lists:append(
        With_session,
        [{header, <<"Authorization"/utf8>>, Auth}]
    ),
    {http_request,
        erlang:element(2, Req),
        erlang:element(3, Req),
        erlang:element(4, Req),
        Final_headers,
        erlang:element(6, Req)}.

-file("src/aws/internal/sigv4.gleam", 342).
-spec merge_query(binary(), list({binary(), binary()})) -> binary().
merge_query(Existing, Auth_params) ->
    Auth_pairs = begin
        _pipe = Auth_params,
        _pipe@1 = gleam@list:map(
            _pipe,
            fun(P) ->
                <<<<(erlang:element(1, P))/binary, "="/utf8>>/binary,
                    (aws@internal@uri:encode_component(erlang:element(2, P)))/binary>>
            end
        ),
        gleam@string:join(_pipe@1, <<"&"/utf8>>)
    end,
    case Existing of
        <<""/utf8>> ->
            Auth_pairs;

        _ ->
            <<<<Existing/binary, "&"/utf8>>/binary, Auth_pairs/binary>>
    end.

-file("src/aws/internal/sigv4.gleam", 335).
-spec host_from_headers(list(aws@internal@http_request:header())) -> binary().
host_from_headers(Headers) ->
    case gleam@list:find(
        Headers,
        fun(H) -> string:lowercase(erlang:element(2, H)) =:= <<"host"/utf8>> end
    ) of
        {ok, H@1} ->
            erlang:element(3, H@1);

        {error, _} ->
            <<""/utf8>>
    end.

-file("src/aws/internal/sigv4.gleam", 211).
?DOC(
    " Build a SigV4 presigned URL — the \"query-string auth\" variant\n"
    " callers reach for to share short-lived links to S3 objects, etc.\n"
    " The auth components (`X-Amz-Algorithm`, `X-Amz-Credential`,\n"
    " `X-Amz-Date`, `X-Amz-Expires`, `X-Amz-SignedHeaders`,\n"
    " `X-Amz-Security-Token` when present, and `X-Amz-Signature`) land\n"
    " in the URL query string rather than headers. Only the `Host`\n"
    " header is signed.\n"
    "\n"
    " `payload_hash` controls the canonical-request payload line:\n"
    "   * `Some(\"UNSIGNED-PAYLOAD\")` — the S3 convention for shared\n"
    "     download URLs (the caller doesn't get to choose the body).\n"
    "   * `Some(hex)` — caller-provided body hash; matches a known\n"
    "     request body that will be sent against the signed URL.\n"
    "   * `None` — the standard SigV4 path, honouring `opts.sign_body`:\n"
    "     `True` ⇒ `sha256(req.body)`, `False` ⇒ `sha256(\"\")` (the\n"
    "     hash of the empty body). The v4 test suite uses this path.\n"
    "\n"
    " `expires_seconds` is bounded by SigV4 to `[1, 604800]` (1 second\n"
    " to 7 days). The function doesn't enforce the bound; AWS rejects\n"
    " out-of-range values at the server side.\n"
    "\n"
    " Returns the full URL (`https://<host><path>?<signed-query>`)\n"
    " ready to hand to a caller. Existing `req.query` entries are\n"
    " preserved and merged with the auth params.\n"
).
-spec presigned_url(
    aws@internal@http_request:http_request(),
    signing_credentials(),
    signing_options(),
    integer(),
    gleam@option:option(binary())
) -> binary().
presigned_url(Req, Creds, Opts, Expires_seconds, Payload_hash) ->
    Host = host_from_headers(erlang:element(5, Req)),
    Date = gleam@string:slice(erlang:element(2, Opts), 0, 8),
    Credential_scope = <<<<<<<<<<<<<<(erlang:element(2, Creds))/binary,
                                "/"/utf8>>/binary,
                            Date/binary>>/binary,
                        "/"/utf8>>/binary,
                    (erlang:element(3, Opts))/binary>>/binary,
                "/"/utf8>>/binary,
            (erlang:element(4, Opts))/binary>>/binary,
        "/aws4_request"/utf8>>,
    Signed_headers_list = aws@internal@sigv4_canonical:signed_headers(
        erlang:element(5, Req)
    ),
    Canonical_headers_block = aws@internal@sigv4_canonical:canonical_headers(
        erlang:element(5, Req)
    ),
    Auth_params = [{<<"X-Amz-Algorithm"/utf8>>, <<"AWS4-HMAC-SHA256"/utf8>>},
        {<<"X-Amz-Credential"/utf8>>, Credential_scope},
        {<<"X-Amz-Date"/utf8>>, erlang:element(2, Opts)},
        {<<"X-Amz-Expires"/utf8>>, erlang:integer_to_binary(Expires_seconds)},
        {<<"X-Amz-SignedHeaders"/utf8>>, Signed_headers_list}],
    Auth_params_for_signing = case {erlang:element(4, Creds),
        erlang:element(7, Opts)} of
        {{some, Token}, false} ->
            lists:append(
                Auth_params,
                [{<<"X-Amz-Security-Token"/utf8>>, Token}]
            );

        {_, _} ->
            Auth_params
    end,
    Merged_query = merge_query(erlang:element(4, Req), Auth_params_for_signing),
    Canonical_uri = aws@internal@sigv4_canonical:build_canonical_uri(
        erlang:element(3, Req),
        erlang:element(5, Opts)
    ),
    Canonical_query = aws@internal@sigv4_canonical:canonical_query_string(
        Merged_query
    ),
    Payload_hash@1 = case Payload_hash of
        {some, H} ->
            H;

        _ ->
            case erlang:element(6, Opts) of
                true ->
                    aws_ffi:hex_encode(aws_ffi:sha256(erlang:element(6, Req)));

                false ->
                    aws_ffi:hex_encode(
                        aws_ffi:sha256(gleam_stdlib:identity(<<""/utf8>>))
                    )
            end
    end,
    Creq = build_creq(
        erlang:element(2, Req),
        Canonical_uri,
        Canonical_query,
        Canonical_headers_block,
        Signed_headers_list,
        Payload_hash@1
    ),
    Sts = string_to_sign(
        Creq,
        erlang:element(2, Opts),
        erlang:element(3, Opts),
        erlang:element(4, Opts)
    ),
    Key = signing_key(
        erlang:element(3, Creds),
        Date,
        erlang:element(3, Opts),
        erlang:element(4, Opts)
    ),
    Sig = signature(Key, Sts),
    Url_with_signature = <<<<<<<<<<<<"https://"/utf8, Host/binary>>/binary,
                        Canonical_uri/binary>>/binary,
                    "?"/utf8>>/binary,
                Canonical_query/binary>>/binary,
            "&X-Amz-Signature="/utf8>>/binary,
        Sig/binary>>,
    case {erlang:element(4, Creds), erlang:element(7, Opts)} of
        {{some, Token@1}, true} ->
            <<<<Url_with_signature/binary, "&X-Amz-Security-Token="/utf8>>/binary,
                (aws@internal@uri:encode_component(Token@1))/binary>>;

        {_, _} ->
            Url_with_signature
    end.