How to sign a URLRequest and download a file from S3 in iOS.
Let’s talk about downloading files from AWS S3 in iOS mobile Apps.
AWS has a batch of services that we can use with our mobile applications. All this can be controlled in developer console with separate Mobile Hub. Also, it has iOS SDK to make the developers' life a bit easier. Actually sometimes not, but it’s another story. Usually, it’s enough to use it from the box as is to achieve the result and have the most popular and common solutions in the Apps.
So if you want to communicate with S3 in a usual manner and download/upload files, I highly recommend using the official AWS SDK for iOS.
But what if we have some restrictions, don’t want to add AWSS3 to our project just to download once a single file or we need to have URLRequest and download something with it.
We have a few options in this case. The easiest one is to make a required file public in a bucket. Such a file will be visible for all who have the correct link. But it’s not secure, and such approach very depends on our needs and type of content. Yes, we can still have a unique and ugly link, so nobody will just guess it. But better to use every possible solution to make content secure and make our users feel safe especially when we can get this just out from the AWS box.
Let’s create a signed request to get a non-public image from our S3 bucket. AWS uses Signature V4 so that we will use it. But old regions can still support Signature V2, if they were created until 10 Jan 2014, according to the official documentation.
At the beginning we need S3. I propose to create it without help from the Mobile Hub side. Just skip this section if you’re familiar with AWS and this process.
I assume that you already have an AWS developer account. Or you can create one for testing. AWS has free tiers, and it’s enough for experimenting with various ideas, learning or even MVP.
We need to select an S3 service from the list of services.
Now we can create a new bucket for testing. Let’s call it downloadimagetestbucket. We can keep all settings by default for now.
We have storage, so let’s upload our test image. Keep all default settings for it, to be sure that it’s not public.
We need AWS credentials to generate request signature. We can use access key ID and secret generated for our root account. But it can be not a good idea, especially if you’re an owner. Such credentials will have full access to everything. We can use them for fast experiments, but not for real-life Apps. We will create separate IAM users only with access to our test bucket.
As before from AWS console with all services list select IAM. From the “Users” tab we can add a new user.
A mythical person with name downloadimagetestuser and Programmatic access.
And then just next… next… next and create. Do not forget to save ID and secret. We have the user without any permission, and he can do nothing in our AWS. Absolutely useless person.
Let’s teach him some tricks. We need to add permission to access S3.
For that, we’ll create a separate policy from the Policies tab.
We can play with a visual editor, but sometimes with JSON, it can be much faster. But in this case, we should know what are we doing; otherwise, it won’t work or even validated.
We will grant only read objects from our S3. You can find more actions in the official documentation. Also, we need our bucket ARN to allow access only to it. You can select an S3 bucket and copy it from info.
JSON looks like
Let’s name it downloadimagetestbucketpolicy. You can add some descriptions too. And then create it. Now we should go back to our created useless user. Select it and in the permission tab select Add permissions.
Select Attach existing policies directly and filter by policy name. Attach the test policy to the user.
Now with this permission, we can use our user.
Implement URLRequest signing in iOS
For creating a request, we will need
File URL, we can find it by selecting the file from our bucket.
IAM user access key ID and secret, we saved them when created new user.
Bucket region, region name is visible when we are selecting the bucket from S3, a list of all regions in the Amazon S3 section. We will need a string like us-east-1.
Some info from the official documentation.
How to get an object from S3. Link
About Authenticating request. Link
How to calculate a signature. Link
The idea is pretty simple. We have some request. We calculate the signature string from it using the user’s secret and passing the user’s ID with the request. AWS will use this ID to find the user and will use the same secret to calculate the signature. And then compare it with the signature from the request. Everything will be OK if it’s the same.
Signature depends on HTTP headers, so let's add some before the actual calculating of the signature.
We will need a SHA256 hash string in hex encoding for the payload; we download a file, so will use empty Data with 0 bytes. Place it with x-amz-content-sha256 header.
Add Content-Type header with image/png string in our case.
Host field which contains service name + region + amazonaws.com Template (serviceName).(region).amazonaws.com Will looks like s3.us-east1.amazonaws.com
X-Amz-Date date with string format yyyyMMdd’T’HHmmss’Z’. Use GMT zone and en_US_POSIX locale for all date strings. Save date somewhere; we will need the same timestamp in a few places.
The last field is the auth Authorization. It contains a signature algorithm, credentials, signed headers and the signature itself. Template AWS4-HMAC-SHA256 Credential=(requestCredentials) SignedHeaders=(signedHeaders) Signature=(signature) Will looks like AWS4-HMAC-SHA256 Credential=AXXXXF6YJEKB2NFZXXXX/20181009/us-east-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=d3402ed5d4d46cea0b3c17e78c421a8afce0a58fd01f24dd77dfb06893613445
Creating the credentials string
The string contains access key ID and request scope. Template (accessKeyID)/(requestScope)
Request scope contains
Date string in short format yyyyMMdd.
Service name, s3 in our case.
And a terminator aws4_request.
We will need the request scope for calculating the signature too.
Scope template (dateString)/(region)/(serviceName)/(terminator)
Will looks like 20181009/us-east-1/s3/aws4_request
Full template (accessKeyID)/(dateString)/(reion)/(serviceName)/(terminator)
Will looks like AXXXXF6YJEKB2NFZXXXX/20181009/us-east-1/s3/aws4_request
Creating a signed headers string
It should contain all headers which we use for calculating the signature.
We need all HTTP fields names from the request, sort them alphabetically in a case-insensitive way and enumerate them with semicolon separator. Headers should be lowercased. And we will use HTTP headers a few times, and each time they should be sorted.
Will looks like content-type;host;x-amz-content-sha256;x-amz-date
Calculating the signature
As we see in documentation steps to calculate the signature
Create canonical request string
Create StringToSign string
Calculate the signature
Creating canonical request string
Each section should be with a new line \n
It containsHTTP method, GET in our case.
URL encoded path /downloadimagetestbucket/TestImage.png
Then goes canonical query string, but it unnecessary for now, so just newline \n.
Canonical headers string As usually should be sorted by lowercased name. Should contain the header name and value separated with a new line \n. And should not contain extra whitespaces. Template (headerName1):(headerValue1)\n(headerName2):(headerValue2) Looks like content-type:image/png\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20181009T115731Z\n
Signed headers string, same as we constructed before.
SHA256 hash string in hex encoding for the request payload, also was calculated before for Data with 0 bytes. We can just reuse it.
Final canonical request string for our test app will look like GET\n/downloadimagetestbucket/TestImage.png\n\ncontent-type:image/png\nhost:s3.us-east-1.amazonaws.com\nx-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nx-amz-date:20181009T115731Z\n\ncontent-type;host;x-amz-content-sha256;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Creating StringToSign string
As for the canonical request, each section should be with a new line \n.
AWS signature algorithm AWS4-HMAC-SHA256.
Date in ISO8601 format yyyyMMdd’T’HHmmss’Z’, we used it before.
Request scope which we used before too. Will looks like 20181009/us-east-1/s3/aws4_request
SHA256 string in hex encoding from our constructed canonical request string
Final StringToSign will looks like AWS4-HMAC-SHA256\n20181009T115731Z\n20181009/us-east-1/s3/aws4_request\n61c352d185e6349d274da84ec475138061572f59d6dbecfcfb7f12fd4c5ce36f
Calculating the signature
As we saw before AWS uses the HMAC-SHA256 algorithm, we can use the CommonCrypto framework or any third party solution to make our life a bit simpler.
Each next step is nested encryption of data with previously generated keys with HMAC-SHA256. hex-encoded
Create DateKey. Encrypt date string in short format yyyyMMdd with the secret key in format AWS4(secretKey)
Create DateRegionKey. Encrypt region string with previously created key DateKey.
Create DateRegionServiceKey. Encrypt service name s3 in our case with created key DateRegionKey.
Create SigningKey. Encrypt terminator aws4_request with created DateRegionServiceKey.
Create a final hex-encoded Signature string. Encrypt created before StringToSign with SigningKey.
And that's all. Now we have everything for the Authorization HTTP header. We can download our file from S3.
You can check the demo project. It’s just an example, recommend not to use it as is because used force unwrapping in some places, but you can adopt this solution for your specific needs and in a more safe manner.