Skip to content

Commit

Permalink
Compatible with AWS signature V4
Browse files Browse the repository at this point in the history
  • Loading branch information
akappen committed Oct 23, 2017
1 parent 2f78fd0 commit a24a3fd
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 60 deletions.
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ config :s3_direct_upload,
aws_access_key: "123abc",
aws_secret_key: "abc123",
aws_s3_bucket: "s3-bucket",
expiration_api: S3DirectUpload.StaticExpiration
date_util: S3DirectUpload.StaticDateUtil
76 changes: 51 additions & 25 deletions lib/s3_direct_upload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ defmodule S3DirectUpload do
Pre-signed S3 upload helper for client-side multipart POSTs.
See: [Browser Uploads to S3 using HTML POST Forms](https://aws.amazon.com/articles/1434/)
See: [Browser-Based Upload using HTTP POST (Using AWS Signature Version 4)](http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html)
[Task 3: Calculate the Signature for AWS Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html)
This module expects three application configuration settings for the
AWS access and secret keys and the S3 bucket name. Here is an
example configuration that reads these from environment
variables. Add your own configuration to `config.exs`.
AWS access and secret keys and the S3 bucket name. You may also
supply an AWS region (the default if you do not is
`us-east-1`). Here is an example configuration that reads these from
environment variables. Add your own configuration to `config.exs`.
```
config :s3_direct_upload,
aws_access_key: System.get_env("AWS_ACCESS_KEY_ID"),
aws_secret_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
aws_s3_bucket: System.get_env("AWS_S3_BUCKET")
aws_s3_bucket: System.get_env("AWS_S3_BUCKET"),
aws_region: System.get_env("AWS_REGION")
```
"""
Expand All @@ -34,18 +36,11 @@ defmodule S3DirectUpload do
Fields that can be over-ridden are:
- `acl` defaults to `public-read`
- `access_key` the AWS access key, defaults to application settings
- `secret_key` the AWS secret key, defaults to application settings
- `bucket` the S3 bucket, defaults to application settings
"""
defstruct file_name: nil, mimetype: nil, path: nil,
acl: "public-read",
access_key: Application.get_env(:s3_direct_upload, :aws_access_key),
secret_key: Application.get_env(:s3_direct_upload, :aws_secret_key),
bucket: Application.get_env(:s3_direct_upload, :aws_s3_bucket)
defstruct file_name: nil, mimetype: nil, path: nil, acl: "public-read"

@expiration Application.get_env(:s3_direct_upload, :expiration_api, S3DirectUpload.Expiration)
@date_util Application.get_env(:s3_direct_upload, :date_util, S3DirectUpload.DateUtil)

@doc """
Expand All @@ -63,8 +58,8 @@ defmodule S3DirectUpload do
iex> %S3DirectUpload{file_name: "image.jpg", mimetype: "image/jpeg", path: "path/to/file"}
...> |> S3DirectUpload.presigned
...> |> Map.get(:credentials) |> Map.get(:AWSAccessKeyId)
"123abc"
...> |> Map.get(:credentials) |> Map.get(:"x-amz-credential")
"123abc/20170101/us-east-1/s3/aws4_request"
iex> %S3DirectUpload{file_name: "image.jpg", mimetype: "image/jpeg", path: "path/to/file"}
...> |> S3DirectUpload.presigned
Expand All @@ -74,13 +69,15 @@ defmodule S3DirectUpload do
"""
def presigned(%S3DirectUpload{} = upload) do
%{
url: "https://#{upload.bucket}.s3.amazonaws.com",
url: "https://#{bucket()}.s3.amazonaws.com",
credentials: %{
AWSAccessKeyId: upload.access_key,
signature: signature(upload),
policy: policy(upload),
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": credential(),
"x-amz-date": @date_util.today_datetime(),
"x-amz-signature": signature(upload),
acl: upload.acl,
key: "#{upload.path}/#{upload.file_name}"
key: file_path(upload)
}
}
end
Expand All @@ -99,13 +96,22 @@ defmodule S3DirectUpload do
end

defp signature(upload) do
:crypto.hmac(:sha, upload.secret_key, policy(upload))
|> Base.encode64
signing_key()
|> hmac(policy(upload))
|> Base.encode16(case: :lower)
end

defp signing_key do
"AWS4#{secret_key()}"
|> hmac(@date_util.today_date())
|> hmac(region())
|> hmac("s3")
|> hmac("aws4_request")
end

defp policy(upload) do
%{
expiration: @expiration.datetime,
expiration: @date_util.expiration_datetime,
conditions: conditions(upload)
}
|> Poison.encode!
Expand All @@ -114,10 +120,30 @@ defmodule S3DirectUpload do

defp conditions(upload) do
[
%{"bucket" => upload.bucket},
%{"bucket" => bucket()},
%{"acl" => upload.acl},
%{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
%{"x-amz-credential": credential()},
%{"x-amz-date": @date_util.today_datetime()},
["starts-with", "$Content-Type", upload.mimetype],
["starts-with", "$key", upload.path]
]
end

defp credential() do
"#{access_key()}/#{@date_util.today_date()}/#{region()}/s3/aws4_request"
end

defp file_path(upload) do
"#{upload.path}/#{upload.file_name}"
end

defp hmac(key, data) do
:crypto.hmac(:sha256, key, data)
end

defp access_key, do: Application.get_env(:s3_direct_upload, :aws_access_key)
defp secret_key, do: Application.get_env(:s3_direct_upload, :aws_secret_key)
defp bucket, do: Application.get_env(:s3_direct_upload, :aws_s3_bucket)
defp region, do: Application.get_env(:s3_direct_upload, :aws_region) || "us-east-1"
end
22 changes: 22 additions & 0 deletions lib/s3_direct_upload/date_util.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule S3DirectUpload.DateUtil do

@moduledoc false

def today_datetime do
%{DateTime.utc_now | hour: 0, minute: 0, second: 0, microsecond: {0,0}}
|> DateTime.to_iso8601(:basic)
end

def today_date do
Date.utc_today
|> Date.to_iso8601(:basic)
end

def expiration_datetime do
DateTime.utc_now()
|> DateTime.to_unix()
|> Kernel.+(60 * 60)
|> DateTime.from_unix!()
|> DateTime.to_iso8601()
end
end
19 changes: 0 additions & 19 deletions lib/s3_direct_upload/expiration.ex

This file was deleted.

31 changes: 31 additions & 0 deletions lib/s3_direct_upload/static_date_util.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule S3DirectUpload.StaticDateUtil do

@moduledoc false

def today_datetime do
static_datetime()
|> DateTime.to_iso8601(:basic)
end

def today_date do
static_date()
|> Date.to_iso8601(:basic)
end

def expiration_datetime do
static_datetime()
|> DateTime.to_unix()
|> Kernel.+(60 * 60)
|> DateTime.from_unix!()
|> DateTime.to_iso8601()
end

defp static_datetime do
~N[2017-01-01 00:00:00]
|> DateTime.from_naive!("Etc/UTC")
end

defp static_date do
~D[2017-01-01]
end
end
8 changes: 0 additions & 8 deletions lib/s3_direct_upload/static_expiration.ex

This file was deleted.

8 changes: 4 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule S3DirectUpload.Mixfile do

def project do
[app: :s3_direct_upload,
version: "0.1.2",
version: "0.1.3",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
Expand All @@ -14,13 +14,12 @@ defmodule S3DirectUpload.Mixfile do

# Configuration for the OTP application
def application do
# Specify extra applications you'll use from Erlang/Elixir
[extra_applications: [:logger]]
end

# Dependencies
defp deps do
[{:poison, "~> 2.0"},
[{:poison, "~> 2.0 or ~> 3.0"},
{:ex_doc, "~> 0.14", only: :dev, runtime: false}]
end

Expand All @@ -38,7 +37,8 @@ defmodule S3DirectUpload.Mixfile do
licenses: ["Apache 2.0"],
links: %{"GitHub" => "https://github.com/akappen/s3_direct_upload",
"S3 Direct Uploads With Ember And Phoenix" => "http://haughtcodeworks.com/blog/software-development/s3-direct-uploads-with-ember-and-phoenix/",
"Browser Uploads to S3 using HTML POST Forms" => "https://aws.amazon.com/articles/1434/"}
"Browser-Based Upload using HTTP POST (Using AWS Signature Version 4)" => "http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html",
"Task 3: Calculate the Signature for AWS Signature Version 4" => "http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html"}
]
end
end
8 changes: 5 additions & 3 deletions test/s3_direct_upload_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ defmodule S3DirectUploadTest do
result = S3DirectUpload.presigned_json(upload) |> Poison.decode!
assert result |> get("url") == "https://s3-bucket.s3.amazonaws.com"
credentials = result |> get("credentials")
assert credentials |> get("AWSAccessKeyId") == "123abc"
assert credentials |> get("signature") == "2XVAeyDzZTVI6XCSGZnoMUab2lI="
assert credentials |> get("policy") |> String.slice(0..9) == "eyJleHBpcm"
assert credentials |> get("acl") == "public-read"
assert credentials |> get("key") == "path/in/bucket/file.jpg"
assert credentials |> get("policy") |> String.slice(0..9) == "eyJleHBpcm"
assert credentials |> get("x-amz-algorithm") == "AWS4-HMAC-SHA256"
assert credentials |> get("x-amz-credential") == "123abc/20170101/us-east-1/s3/aws4_request"
assert credentials |> get("x-amz-date") == "20170101T000000Z"
assert credentials |> get("x-amz-signature") == "1c1210287ea2cb1c915ee11b9515b2d811f4b21a90e78a45f12465974ebb95f1"
end
end

0 comments on commit a24a3fd

Please sign in to comment.