Amazon Web Services REST queries signature in ActionScript 3
As I was working on a custom MP3 player, I wanted to use a web service to search for album arts in order to display them as the corresponding music is played. After a quic Google search I found Amazon Web Services (AWS). Amazon actually provides services to search items in their catalog and therefor you can look for a precise music album and get its cover. This very service is the Product Advertising and is accessible through the Product Advertising API.
But since the 15th of August 2009 AWS requires every request to be signed. It simply means that a "Signature" parameter must be added to each and every request. One might think that the API key should be enough, but this key is inserted in every query URL and can be read by pretty much anyone. The signature is created using the secret AWS password and crypted using the HMAC/SHA256 algorithms so that AWS can be sure the request actually comes from the genuine owner of the API key.
The documentation features a guide to explain the whole signing process step-by-step but as you might have guesses, there is a huge difference between reading it on the documentation and actually implementing it successfully.
Full code right after the jump...
The following class wrap the URLRequest class to provide signed requests to AWS (requires AS3CoreLib):
package com.amazon { import com.adobe.crypto.HMAC; import com.adobe.crypto.SHA256; import flash.net.URLRequest; import flash.utils.ByteArray; import mx.formatters.DateFormatter; import mx.utils.Base64Encoder; public class AmazonRequest { public static const AWS_NAMESPACE : Namespace = new Namespace("http://webservices.amazon.com/AWSECommerceService/2009-01-06"); private static const AWS_KEY : String = "your_aws_api_key_here"; private static const AWS_SECRET : String = "your_aws_secret_key_here"; private static const AWS_METHOD : String = "GET"; private static const AWS_HOST : String = "ecs.amazonaws.com"; private static const AWS_PATH : String = "/onca/xml"; private static const AWS_URL : String = "http://" + AWS_HOST + AWS_PATH; private var _URLRequest : URLRequest = new URLRequest(); private var _DateFormatter : DateFormatter = new DateFormatter(); public function get url() : String {return (_URLRequest.url);} public function get data() : Object {return (_URLRequest.data);} public function get requestHeaders() : Array {return (_URLRequest.requestHeaders);} public function get urlRequest() : URLRequest {return (signRequest());} public function set url(value : String) : void {_URLRequest.url = value;} public function set data(value : Object) : void {_URLRequest.data = value;} public function set requestHeader(value : Array) : void {_URLRequest.requestHeaders = value;} public function AmazonRequest() { _URLRequest.url = AWS_URL; _DateFormatter.formatString = "YYYY-MM-DDTJJ:NN:SS.000Z"; } private function parametersSort(s1 : Object, s2 : Object) : Number { var c1 : Number = (s1.name as String).charCodeAt(0); var c2 : Number = (s2.name as String).charCodeAt(0); return (c1 > c2 ? 1.0 : -1.0); } private function signRequest() : URLRequest { var vars : Array = []; var signature : String = AWS_METHOD + "\n" + AWS_HOST + "\n" + AWS_PATH + "\n"; var timestamp : String = null; var encoder : Base64Encoder = new Base64Encoder(); var secretBytes : ByteArray = new ByteArray(); var requestBytes : ByteArray = new ByteArray(); var now : Date = new Date(); var hash : String = null; var hashBytes : ByteArray = new ByteArray(); // build the timestamp now.setTime(now.getTime() + (now.getTimezoneOffset() * 60 * 1000)); timestamp = _DateFormatter.format(now); vars.push({name: "Timestamp", value: encodeURIComponent(decodeURIComponent(timestamp))}); // add the API Key _URLRequest.data.AWSAccessKeyId = AWS_KEY; // extract and sort parameters for (var o : Object in _URLRequest.data) vars.push({name: o, value: _URLRequest.data[o]}); vars = vars.sortOn("name"); // build the string to sign for (var i : int = 0; i < vars.length; i++) signature += (i ? "&" : "") + encodeURIComponent(decodeURIComponent(vars[i].name)) + "=" + encodeURIComponent(decodeURIComponent(vars[i].value)); // build the signature hash = HMAC.hash(AWS_SECRET, signature, SHA256); for (i = 0; i < hash.length; i += 2) hashBytes.writeByte(parseInt(hash.charAt(i) + hash.charAt(i + 1), 16)); // encode the signature to Base64 encoder.encodeBytes(hashBytes); // the the Signature and Timestamp parameters _URLRequest.data.Signature = encodeURI(encoder.toString()); _URLRequest.data.Timestamp = timestamp; return (_URLRequest); } } }
... and the following example function demonstrates how to use this class to actually query AWS to get album arts from a given album name:
public function getCovers(myAlbum : String) : void { var request : AmazonRequest = new AmazonRequest(); var loader : URLLoader = new URLLoader(); var vars : URLVariables = new URLVariables(); vars.Service = 'AWSECommerceService'; vars.Operation = 'ItemSearch'; vars.Keywords = myAlbum; vars.Version = "2009-01-06"; vars.SearchIndex = "Music"; vars.ResponseGroup = "Images"; request.data = vars; loader.addEventListener(Event.COMPLETE, requestCompleteHandler); loader.load(request.urlRequest); }
And voila, I hope this will help!
August 28th, 2009 - 13:55
Hi
thanks for posting this, just was looking for some example simplifying the migration of my AS3 Amazon application to the new API. Yet as I also was not aware of at first did you thought about the problem that somebody might disassemble the flash movie and by that gets to know your secret key. See for example
http://developer.amazonwebservices.com/connect/thread.jspa?messageID=132059𠏛
where this problem is addressed. Since there was no answer from amazons side in the thread mentioned I reposted the question
http://developer.amazonwebservices.com/connect/thread.jspa?threadID=35714&tstart=0
hope to get some more definite answer on that
cheers and thanks for sharing source code
martin
August 28th, 2009 - 14:47
@martin
The ability to decompile source code that contains sensitive data has always been a problem.
You might want to take a look at this:
[AS3] Hiding Assets And Code By Embedding SWF Within Another SWF.
August 28th, 2009 - 20:03
Thanks Promethe for the link, will look into it. Happy to see that you are aware of the problem, didn’t want to leave this forum without addressing it for the case you or who ever comes along here don’t have it in the back of his mind.
I guess its hard to say how safe this embedding technique is, so most likely I will go over a server side service to be on the safe side. Just really a pity that amazon went that way with their API. At least they should address this issue big time in their new API introduction.
August 29th, 2009 - 10:05
I guess there is no foolproof solution… still, using a few simple tricks will hold back the average wanabe hacker. The rest of it will always find its way anyway I guess…
September 9th, 2009 - 21:16
Hey….I am trying to use this code but I can’t get it to work. I just keep getting this error in the repsonse:
“The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.”
Got any advise?
September 12th, 2009 - 14:29
Make sure you are using your proper AWS Secret Key and AWS API Key
September 14th, 2009 - 10:46
The code works fine, but I’m unable to parse the resulting XML:
var AmazonXML:XML = XML(loader.data); //Returns an XML with all data
amazonResult = AmazonXML.ItemSearchResponse.Items; //Returns nothing
amazonResult = AmazonXML.Items; //Returns nothing
September 15th, 2009 - 00:03
Be careful: all the nodes are in the AWS namespace! Therefor, you have to use my AWS_NAMESPACE constant to parse the XML data (its why I added it in the first place if I remember correctly).
Try:
var AmazonXML:XML = XML(loader.data);
amazonResult = AmazonXML..AWS_NAMESPACE::Items;
October 13th, 2009 - 11:46
Does anyone have a package that has this code fully working? Basically what I want is a web service that allows a user to type in a book title, then the screen to populate with thumbnails.
Ideally once this happens, various details will appear with the book. eg. Publisher, year, etc.
Thanks in advance for the help.
October 14th, 2009 - 00:13
The code IS fully working
Just copy/paste it… and proceed as in the example. You’ll have to change the “SearchIndex” parameter to “Book” (it’s just an educated guess, check the API’s documentation to be sure).
One detail though… Amazon image servers do not provide any crossdomain.xml file so you can get the pictures but not modify them :s. You have to display them “as is” which is very problematic. The only solution is to use a proxy (a PHP proxy for example).
You can also post here!
October 14th, 2009 - 10:54
Thanks for replying mate. Sorry what I meant by “package” was perhaps a zip file containing all the necessary files to get this working. This would include the .fla as well as other associated classes needed.
Thanks again.
October 31st, 2009 - 06:27
Where do we place the example? After the last function? Or in a different file?
October 31st, 2009 - 21:32
It’s a sample code… it doesn’t have to be placed anywhere. It is just a sample function that describes how to use the AmazonRequest class.
November 18th, 2009 - 22:43
Nicely done Promethe!
February 2nd, 2010 - 02:58
Hey,
Thanks a lot for sharing this class, figuring this out all by myself would’ve cost me loads of time!
A small note regarding the generation of the timestamp:
I received an “invalid timestamp error” a few minutes after midnight, so as the timezone offset here is GMT+1 (Netherlands) I’m assuming the API listens to the clients’ local time (or the request is directed to a mirror).
now.setTime(now.getTime() + (now.getTimezoneOffset() * 60 * 1000));
should be simplified to
now.setTime(now.getTime());
Thomas
April 23rd, 2010 - 05:23
I am hoping that you could explain to me this part of the function signrequest():
hashBytes.writeByte(parseInt(hash.charAt(i) + hash.charAt(i + 1), 16));
As I understand it, the SHA256 string has to be converted into bytes. Where I am lost is why you used a base 16 int and why two chars are added together. Maybe you could point me in the right direction.
Jonathan
April 23rd, 2010 - 12:30
@Jonathan
The goal is to convert the hexadecimal string into a binary byte array because this is what the Base64 encoder expects.
Each byte is two string caracters (from 00 to FF). I take two caracters at a time, concatenate them to get a full byte and convert it into its hexadecimal integer value using parseInt(string, base). Then, I write the result in the byte array.
April 23rd, 2010 - 22:50
Yes, that makes perfect sense. I wasn’t aware that the output of the HMAC was a hexadecimal string. Thanks for your time.
May 7th, 2010 - 03:12
thanks for this, very helpful in updating my old (and broken due to needing Signature) AWS calls.
i think i found one problem. i believe the DateFormatter’s format string should be
“YYYY-MM-DDTJJ:NN:SS.000Z”
note the J’s instead of H’s. i had a problem when my local time was before midnight but adding in hours to get to UTC time made it the first hour of the next day, as the following response error shows.
”
InvalidParameterValueValue 2010-05-07T24:25:20.000Z for parameter Timestamp is invalid. Reason: Must be in ISO8601 format.”note the time being listed as 24:25:20. ISO8601 doesn’t appear to accept a time with an hour specified as 24 unless the time is exactly 24:00:00. it expects 00:25:20 for such a time. if i switched H (1-24 hour) to J (0-23 hour) that avoided the problem. this might also be why Thomas above had a problem around midnight.
May 7th, 2010 - 22:13
@Mike: Thank you! I updated the code.
May 25th, 2010 - 21:11
hi, guys iam noob here and want your help
i am trying to develop flex application that gets books from amazon
when i use this class i am keep getting this error message
“The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.”
i checked AWS Secret Access Key but it is correct
please any help as soon as possible …..please
see the error:
http://www.1ss1.com/download.php?img=1124
May 27th, 2010 - 07:26
Hi everyone
I’m in the same boat as ahmed, as I am also repeatedly getting the SignatureDoesNotMatch error. I’ve created new keys, but I am getting the same issue.
Is anyone else running into this?
Thanks