From 275ca1e65dc346843d0ef92dcf0e0da1c60c1981 Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Fri, 26 Jun 2015 10:16:32 -0400 Subject: [PATCH 1/4] Internalize authentication code for easier adaptation. --- lib/ex_aws/auth.ex | 112 +++++++++++++++++++++++++++++++++++++++ lib/ex_aws/auth/utils.ex | 32 +++++++++++ lib/ex_aws/request.ex | 40 +------------- lib/ex_aws/s3/request.ex | 2 +- mix.exs | 2 +- mix.lock | 9 ++-- 6 files changed, 151 insertions(+), 46 deletions(-) create mode 100644 lib/ex_aws/auth.ex create mode 100644 lib/ex_aws/auth/utils.ex diff --git a/lib/ex_aws/auth.ex b/lib/ex_aws/auth.ex new file mode 100644 index 00000000..8a809e70 --- /dev/null +++ b/lib/ex_aws/auth.ex @@ -0,0 +1,112 @@ +defmodule ExAws.Auth do + import ExAws.Auth.Utils + alias Timex.DateFormat + + def headers(http_method, url, service, config, headers, body) do + now = %{Timex.Date.now | ms: 0} + headers = [ + {"host", URI.parse(url).host}, + {"x-amz-date", amz_date(now)} | + headers + ] + + auth_header = ExAws.Auth.auth_header( + config[:access_key_id], + config[:secret_access_key], + http_method |> method_string, + url, + config[:region], + service |> service_name, + headers |> Enum.into(%{}), + body, + now) + + [{"Authorization", auth_header} | headers ] + end + + def auth_header(access_key, secret_key, http_method, url, region, service, headers, body, now) do + date = DateFormat.format!(now, "%Y%m%d", :strftime) + scope = "#{date}/#{region}/#{service}/aws4_request" + + signing_key = build_signing_key(secret_key, date, region, service) + + signature = http_method + |> build_canonical_request(url, headers, body) + |> build_string_to_sign(now, scope) + |> sign_with(signing_key) + + [ + "AWS4-HMAC-SHA256 Credential=", access_key, "/", scope, ",", + "SignedHeaders=", signed_headers(headers), ",", + "Signature=", signature + ] |> IO.iodata_to_binary + end + + def build_signing_key(secret_key, date, region, service) do + ["AWS4", secret_key] + |> hmac_sha256(date) + |> hmac_sha256(region) + |> hmac_sha256(service) + |> hmac_sha256("aws4_request") + end + + def build_string_to_sign(canonical_request, now, scope) do + timestamp = now |> ExAws.Auth.Utils.amz_date + hashed_canonical_request = ExAws.Auth.Utils.hash_sha256(canonical_request) + + "AWS4-HMAC-SHA256\n#{timestamp}\n#{scope}\n#{hashed_canonical_request}" + end + + def sign_with(string_to_sign, signing_key) do + signing_key + |> hmac_sha256(string_to_sign) + |> bytes_to_hex + end + + def signed_headers(headers) do + headers + |> Enum.map(fn({k, _}) -> String.downcase(k) end) + |> Enum.sort(&(&1 < &2)) + |> Enum.join(";") + end + + def build_canonical_request(http_method, url, headers, body) do + uri = URI.parse(url) + http_method = http_method |> String.upcase + + query_params = uri.query |> canonical_query_params + + headers = headers |> canonical_headers + header_string = headers + |> Enum.map(fn {k, v} -> "#{k}:#{v}" end) + |> Enum.join("\n") + + signed_headers_list = headers + |> Keyword.keys + |> Enum.join(";") + + [ + http_method, "\n", + uri.path, "\n", + query_params, "\n", + header_string, "\n", + "\n", + signed_headers_list, "\n", + ExAws.Auth.Utils.hash_sha256(body) + ] |> IO.iodata_to_binary + end + + def canonical_query_params(nil), do: "" + def canonical_query_params(params) do + params + |> URI.query_decoder + |> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end) + |> URI.encode_query + end + + def canonical_headers(headers) do + headers + |> Enum.map(fn {k, v} -> {String.downcase(k), String.strip(v)} end) + |> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end) + end +end diff --git a/lib/ex_aws/auth/utils.ex b/lib/ex_aws/auth/utils.ex new file mode 100644 index 00000000..36d70227 --- /dev/null +++ b/lib/ex_aws/auth/utils.ex @@ -0,0 +1,32 @@ +defmodule ExAws.Auth.Utils do + def amz_date(now) do + now + |> Timex.DateFormat.format!("{ISOz}") + |> String.replace("-", "") + |> String.replace(":", "") + end + + def hash_sha256(data) do + :sha256 + |> :crypto.hash(data) + |> bytes_to_hex + end + + def hmac_sha256(key, data) do + :crypto.hmac(:sha256, key, data) + end + + def bytes_to_hex(bytes) do + bytes + |> Base.encode16 + |> String.downcase + end + + def service_name(service), do: service |> Atom.to_string + + def method_string(method) do + method + |> Atom.to_string + |> String.upcase + end +end diff --git a/lib/ex_aws/request.ex b/lib/ex_aws/request.ex index 4e7f594d..6d4efe0c 100644 --- a/lib/ex_aws/request.ex +++ b/lib/ex_aws/request.ex @@ -16,42 +16,10 @@ defmodule ExAws.Request do _ -> config[:json_codec].encode!(data) end - headers = headers(http_method, url, service, config, headers, body) + headers = ExAws.Auth.headers(http_method, url, service, config, headers, body) request_and_retry(http_method, url, service, config, headers, body, {:attempt, 1}) end - def headers(http_method, url, service, config, headers, body) do - now = %{Timex.Date.now | ms: 0} - amz_date = Timex.DateFormat.format!(now, "{ISOz}") - |> String.replace("-", "") - |> String.replace(":", "") - - headers = [ - {"host", URI.parse(url).host}, - {"x-amz-date", amz_date} | - headers - ] - - auth_header = AWSAuth.sign_authorization_header( - config[:access_key_id], - config[:secret_access_key], - http_method |> method_string, - url, - config[:region], - service |> service_name, - headers |> Enum.into(%{}), - body, - now) - - [{"Authorization", auth_header} | headers ] - end - - def service_name(service), do: service |> Atom.to_string - - def binary_headers(headers) do - headers |> Enum.map(fn({k, v}) -> {List.to_string(k), List.to_string(v)} end) - end - @doc false def request_and_retry(_method, _url, _service, _config, _headers, _req_body, {:error, reason}), do: {:error, reason} @@ -122,10 +90,4 @@ defmodule ExAws.Request do def backoff(attempt) do :timer.sleep(attempt * 1000) end - - defp method_string(method) do - method - |> Atom.to_string - |> String.upcase - end end diff --git a/lib/ex_aws/s3/request.ex b/lib/ex_aws/s3/request.ex index 8ac4acfc..f0f23bf7 100644 --- a/lib/ex_aws/s3/request.ex +++ b/lib/ex_aws/s3/request.ex @@ -13,7 +13,7 @@ defmodule ExAws.S3.Request do |> url(bucket, path) |> add_query(resource, query) - hashed_payload = AWSAuth.Utils.hash_sha256(body) + hashed_payload = ExAws.Auth.Utils.hash_sha256(body) headers = [ {"x-amz-content-sha256", hashed_payload} | diff --git a/mix.exs b/mix.exs index e13df999..d8186f6a 100644 --- a/mix.exs +++ b/mix.exs @@ -20,7 +20,7 @@ defmodule ExAws.Mixfile do defp deps do [ - {:aws_auth, "~> 0.2.3"} | + {:timex, "~> 0.13.4"} | deps(:test_dev) ] end diff --git a/mix.lock b/mix.lock index 7f197d97..4c019d80 100644 --- a/mix.lock +++ b/mix.lock @@ -1,14 +1,13 @@ -%{"aws_auth": {:hex, :aws_auth, "0.2.3"}, - "earmark": {:hex, :earmark, "0.1.15"}, - "ex_doc": {:hex, :ex_doc, "0.7.2"}, +%{"earmark": {:hex, :earmark, "0.1.17"}, + "ex_doc": {:hex, :ex_doc, "0.7.3"}, "hackney": {:hex, :hackney, "1.1.0"}, "httpoison": {:hex, :httpoison, "0.6.2"}, "httpotion": {:hex, :httpotion, "2.0.0"}, "ibrowse": {:git, "git://github.com/cmullaparthi/ibrowse.git", "d2e369ff42666c3574b8b7ec26f69027895c4d94", [tag: "v4.1.1"]}, "idna": {:hex, :idna, "1.0.2"}, "jsx": {:hex, :jsx, "2.5.3"}, - "mixunit": {:hex, :mixunit, "0.9.1"}, + "mixunit": {:hex, :mixunit, "0.9.2"}, "poison": {:hex, :poison, "1.2.1"}, - "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.4"}, + "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}, "sweet_xml": {:hex, :sweet_xml, "0.2.1"}, "timex": {:hex, :timex, "0.13.4"}} From b1f4be02c09a3892c69a84495c5257fde4593465 Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Fri, 26 Jun 2015 11:24:42 -0400 Subject: [PATCH 2/4] minor cleanup --- lib/ex_aws/auth.ex | 4 ++-- mix.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ex_aws/auth.ex b/lib/ex_aws/auth.ex index 8a809e70..7dc36007 100644 --- a/lib/ex_aws/auth.ex +++ b/lib/ex_aws/auth.ex @@ -10,14 +10,14 @@ defmodule ExAws.Auth do headers ] - auth_header = ExAws.Auth.auth_header( + auth_header = auth_header( config[:access_key_id], config[:secret_access_key], http_method |> method_string, url, config[:region], service |> service_name, - headers |> Enum.into(%{}), + headers, body, now) diff --git a/mix.exs b/mix.exs index d8186f6a..4225fc42 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,7 @@ defmodule ExAws.Mixfile do end def application do - [applications: [:logger], + [applications: [:logger, :timex], mod: {ExAws, []}] end From 721ab680619c2e16ca8a6bb7cdacbefbc2b6cdee Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Fri, 26 Jun 2015 11:41:15 -0400 Subject: [PATCH 3/4] add in crypto dep for release compliance --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 4225fc42..111fe58d 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,7 @@ defmodule ExAws.Mixfile do end def application do - [applications: [:logger, :timex], + [applications: [:logger, :timex, :crypto], mod: {ExAws, []}] end From 55509f127edf50ebd0300514b32a5137a4d9ed2c Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Fri, 26 Jun 2015 11:43:10 -0400 Subject: [PATCH 4/4] more consistency WRT to building iodata --- lib/ex_aws/auth.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ex_aws/auth.ex b/lib/ex_aws/auth.ex index 7dc36007..dec6b65d 100644 --- a/lib/ex_aws/auth.ex +++ b/lib/ex_aws/auth.ex @@ -54,7 +54,12 @@ defmodule ExAws.Auth do timestamp = now |> ExAws.Auth.Utils.amz_date hashed_canonical_request = ExAws.Auth.Utils.hash_sha256(canonical_request) - "AWS4-HMAC-SHA256\n#{timestamp}\n#{scope}\n#{hashed_canonical_request}" + [ + "AWS4-HMAC-SHA256", "\n", + timestamp, "\n", + scope, "\n", + hashed_canonical_request + ] |> IO.iodata_to_binary end def sign_with(string_to_sign, signing_key) do