Promethe’s Blog Web, RIAs and chocolate spaghettis…

24Aug/0922

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!

Comments (22) Trackbacks (0)
  1. 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&#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

  2. @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.

  3. 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.

  4. 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…

  5. 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?

  6. Make sure you are using your proper AWS Secret Key and AWS API Key :D

  7. 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

  8. 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;

  9. 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.

  10. The code IS fully working :D 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!

  11. 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.

  12. Where do we place the example? After the last function? Or in a different file?

  13. 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.

  14. Nicely done Promethe!

  15. 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

  16. 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

  17. @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.

  18. Yes, that makes perfect sense. I wasn’t aware that the output of the HMAC was a hexadecimal string. Thanks for your time.

  19. 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.

  20. @Mike: Thank you! I updated the code.

  21. 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

  22. 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


Leave a comment


No trackbacks yet.