Directly Uploading to S3 from iPhone

27 Aug 2013

This post is my reply, in blog form, to Architectural and design question about uploading photos from iPhone app and S3 on Stack Overflow. The issue addressed is how to upload a file from the iPhone directly to S3 without potentially exposing your S3 secret key to jailbroken users, which could then add or remove keys or entire buckets.

You can upload directly from your iPhone to S3 using the REST API, and have the server be responsible for generating the part of the Authorization header value that requires the secret key. This way, you don’t risk exposing the access key to anyone with a jailbroken iPhone, while you don’t put the burden of uploading the file on the server. The exact details of the request to make can be found under “Example Object PUT” of “Signing and Authenticating REST Requests.” I strongly recommend reading that document before proceeding any further.

The following code, written in Python, generates the part of the Authorization header value that is derived from your S3 secret access key. You should substitute your own secret access key and bucket name in virtual host form for _S3_SECRET and _S3_BUCKET_NAME below, respectively:

import base64
from datetime import datetime
import hmac
import sha

# Replace these values.
_S3_SECRET = "my_s3_secret"
_S3_BUCKET_NAME = "my-bucket-name"

def get_upload_header_values(content_type, filename): 
	now = datetime.utcnow()
	date_string = now.strftime("%a, %d %b %Y %H:%M:%S +0000")
	full_pathname = '/%s/%s' % (_S3_BUCKET_NAME, filename)
	string_to_sign = "PUT\n\n%s\n%s\n%s" % (
			content_type, date_string, full_pathname)
	h = hmac.new(_S3_SECRET, string_to_sign, sha)
	auth_string = base64.encodestring(h.digest()).strip()
	return (date_string, auth_string)

Calling this with the filename foo.txt and content-type text/plain yields:

>>> get_upload_header_values('text/plain', 'foo.txt')
('Wed, 06 Feb 2013 00:57:45 +0000', 'EUSj3g70aEsEqSyPT/GojZmY8eI=')

Note that if you run this code, the time returned will be different, and so the encoded HMAC digest will be different.

Now the iPhone client just has to issue a PUT request to S3 using the returned date and HMAC digest. Assuming that:

Then you can do the following to create the NSURLRequest:

NSString *serverDate = [serverJson objectForKey:@"date"]
NSString *serverHmacDigest = [serverJson objectForKey:@"hmacDigest"]

// Create the headers.
NSMutableDictionary *headers = [[NSMutableDictionary alloc] init];
[headers setObject:contentType forKey:@"Content-Type"];
NSString *host = [NSString stringWithFormat:@"%@.s3.amazonaws.com",
                  kS3BucketName]
[headers setObject:host forKey:@"Host"];
[headers setObject:serverDate forKey:@"Date"];
NSString *authorization = [NSString stringWithFormat:@"AWS %@:%@",
                           kS3AccessKey,
                           serverHmacDigest];
[headers setObject:authorization forKey:@"Authorization"];

// Create the request.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:data];
[request setHTTPMethod:@"PUT"];
NSString *postUrl = [NSString stringWithFormat:@"http://%@.s3.amazonaws.com/%@",
                     kS3BucketName,
                     filename];
[request setURL:[NSURL URLWithString:postUrl]];

Finally, issue the request. If you’re using the excellent AFNetworking library, then you can wrap request in an AFXMLRequestOperation object using XMLDocumentRequestOperationWithRequest:success:failure:, and invoke its start method. Don’t forget to release headers and request when done.

Note that the client got the value of the Date header from the server. This is because, as Amazon describes under “Time Stamp Requirement”:

“A valid time stamp (using either the HTTP Date header or an x-amz-date alternative) is mandatory for authenticated requests. Furthermore, the client time-stamp included with an authenticated request must be within 15 minutes of the Amazon S3 system time when the request is received. If not, the request will fail with the RequestTimeTooSkewed error status code.”

So instead of relying on the client having the correct time in order for the request to succeed, rely on the server, which should be using NTP (and a daemon like ntpd).

comments powered by Disqus