Skip to content

Commit

Permalink
Merge pull request #244 from d3caf/hash-headers
Browse files Browse the repository at this point in the history
Append x-amz-checksum headers when using hashing algorithm other than MD5
  • Loading branch information
bernardd authored Mar 19, 2024
2 parents 96d3f44 + 6d4712a commit bd5feea
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 5 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ config :ex_aws, :s3,
port: 9000
```

An alternate `content_hash_algorithm` can be specified as well. The default is `:md5`. It may be necessary to change this when operating in a FIPS-compliant environment where MD5 isn't available, for instance. At this time, only `:sha256`, `:sha`, and `:md5` are supported by both Erlang and S3.

``` elixir
# config.exs
config :ex_aws_s3, :content_hash_algorithm, :sha256
```

<!-- MDOC !-->

## License
Expand Down
18 changes: 14 additions & 4 deletions lib/ex_aws/s3.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ defmodule ExAws.S3 do

@type amz_meta_opts :: [{atom, binary} | {binary, binary}, ...]

@typedoc """
The hashing algorithms that both S3 and Erlang support.
https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
https://www.erlang.org/doc/man/crypto.html#type-hash_algorithm
"""
@type hash_algorithm :: :sha | :sha256 | :md5

## Buckets
#############
@doc "List buckets"
Expand Down Expand Up @@ -1006,15 +1014,17 @@ defmodule ExAws.S3 do
{hash_header(alg), :crypto.hash(alg, content) |> Base.encode64()}
end

@spec get_hash_config() :: :md5
@spec get_hash_config() :: hash_algorithm()
defp get_hash_config() do
Application.get_env(:ex_aws_s3, :content_hash_algorithm) || :md5
end

# Supported hash algorithms:
# Supported erlang hash algorithms:
# https://www.erlang.org/doc/man/crypto.html#type-hash_algorithm
@spec hash_header(atom()) :: binary()
defp hash_header(alg) when is_atom(alg), do: "content-#{to_string(alg)}"
@spec hash_header(hash_algorithm()) :: binary()
defp hash_header(:md5), do: "content-md5"
defp hash_header(:sha), do: "x-amz-checksum-sha1"
defp hash_header(alg) when is_atom(alg), do: "x-amz-checksum-#{to_string(alg)}"

@spec pair_tuple_to_map({term(), term()}) :: map()
defp pair_tuple_to_map(tuple), do: Map.new([tuple])
Expand Down
77 changes: 76 additions & 1 deletion test/lib/s3_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule ExAws.S3Test do
use ExUnit.Case, async: true
# Setting async: false since we're modifying Application env
use ExUnit.Case, async: false
alias ExAws.{S3, Operation}

test "#list_objects" do
Expand Down Expand Up @@ -337,6 +338,50 @@ defmodule ExAws.S3Test do
])
end

test "#delete_multiple_objects sha1" do
set_content_hash_algorithm(:sha)

expected = %Operation.S3{
body:
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Delete><Object><Key>foo</Key></Object><Object><Key>bar</Key><VersionId>v1</VersionId></Object><Object><Key>special characters: &apos;&quot;&amp;&lt;&gt;&#13;&#10;</Key></Object></Delete>",
bucket: "bucket",
path: "/?delete",
headers: %{"x-amz-checksum-sha1" => "1uLbLBmwtufR1/csrQPCAONFeKU="},
http_method: :post
}

assert expected ==
S3.delete_multiple_objects("bucket", [
"foo",
{"bar", "v1"},
"special characters: '\"&<>\r\n"
])

on_exit(&unset_content_hash_algorithm/0)
end

test "#delete_multiple_objects sha256" do
set_content_hash_algorithm(:sha256)

expected = %Operation.S3{
body:
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Delete><Object><Key>foo</Key></Object><Object><Key>bar</Key><VersionId>v1</VersionId></Object><Object><Key>special characters: &apos;&quot;&amp;&lt;&gt;&#13;&#10;</Key></Object></Delete>",
bucket: "bucket",
path: "/?delete",
headers: %{"x-amz-checksum-sha256" => "Hsce+MZp64Uy8CwyResJ3y13YGw6jDAAdVGFmFPKPSs="},
http_method: :post
}

assert expected ==
S3.delete_multiple_objects("bucket", [
"foo",
{"bar", "v1"},
"special characters: '\"&<>\r\n"
])

on_exit(&unset_content_hash_algorithm/0)
end

test "#post_object_restore" do
expected = %Operation.S3{
body:
Expand Down Expand Up @@ -697,6 +742,36 @@ defmodule ExAws.S3Test do
expected = %{"content-md5" => content_hash}
assert ExAws.S3.calculate_content_header("hello world") === expected
end

test "SHA1" do
set_content_hash_algorithm(:sha)

body = "hello world"
content_hash = :crypto.hash(:sha, body) |> Base.encode64()
expected = %{"x-amz-checksum-sha1" => content_hash}
assert ExAws.S3.calculate_content_header("hello world") === expected

on_exit(&unset_content_hash_algorithm/0)
end

test "SHA256" do
set_content_hash_algorithm(:sha256)

body = "hello world"
content_hash = :crypto.hash(:sha256, body) |> Base.encode64()
expected = %{"x-amz-checksum-sha256" => content_hash}
assert ExAws.S3.calculate_content_header("hello world") === expected

on_exit(&unset_content_hash_algorithm/0)
end
end

defp set_content_hash_algorithm(alg) do
Application.put_env(:ex_aws_s3, :content_hash_algorithm, alg)
end

defp unset_content_hash_algorithm() do
Application.delete_env(:ex_aws_s3, :content_hash_algorithm)
end

@spec assert_pre_signed_url(
Expand Down

0 comments on commit bd5feea

Please sign in to comment.