Amazon Product Advertising API Signature in iOS
Asked Answered
L

8

14

I am trying to access Amazon's Product Advertising API in my iOS application. Creating the signature seems to be the tough part. On this page:

http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/rest-signature.html

It says to "Calculate an RFC 2104-compliant HMAC with the SHA256 hash algorithm". Amazon also provides a java class to do this for you:

http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/AuthJavaSampleSig2.html

How can I do this in Objective-C instead? I looked into the AWS iOS SDK, but it doesn't seem to include the Product Advertising API.

Legitimist answered 7/6, 2012 at 21:39 Comment(0)
C
11

Actually the AWS iOS SDK DID have a static method to handle all auth situations. Maybe you should take a glance at the AmazonAuthUtils.h :

+(NSString *)HMACSign:(NSData *)data withKey:(NSString *)key usingAlgorithm:(CCHmacAlgorithm)algorithm;
+(NSData *)sha256HMac:(NSData *)data withKey:(NSString *)key;

you can find it in the document: http://docs.amazonwebservices.com/AWSiOSSDK/latest/Classes/AmazonAuthUtils.html

Cb answered 28/6, 2012 at 8:44 Comment(1)
The link no longer works, and documentation for the AWS frameworks no longer contains or mentions anything resembling AmazonAuthUtils. The 100-page doc, for example, has no mention of the word 'signature.' Any updated versions?Becoming
P
11

Just to add a bit to camelcc's excellent observation. This does indeed work well for signing requests to the Amazon Product Advertising API. I had to mess around a bit to get it working.

Get the SDK installed and #import <AWSiOSSDK/AmazonAuthUtils.h>

First you've got to organize the request string into the correct order, as per the Amazon docs. I found this page very useful in explaining how to order the request

http://skilldrick.co.uk/2010/02/amazon-product-information-via-amazon-web-services/

Note the need for new-line characters in the string, my unsigned string looked like this

@"GET\necs.amazonaws.com\n/onca/xml\nAWSAccessKeyId=<ACCESS_KEY_ID>&AssociateTag=<ASSOCIATE_ID>&Keywords=harry%20potter&Operation=ItemSearch&SearchIndex=Books&Service=AWSECommerceService&Timestamp=2012-07-03T10%3A52%3A21.000Z&Version=2011-08-01"

No spaces anywhere, but \n characters in the right places. The convert this to NSData like so

NSData *dataToSign = [unsignedString dataUsingEncoding:NSUTF8StringEncoding];

Then call

[AmazonAuthUtils HMACSign:dataToSign withKey:SECRET_KEY usingAlgorithm:kCCHmacAlgSHA256]

This returns your signature as an NSString. You'll need to URL encode this (ie swapping illegal/unsafe charecters for %0x symbols (ie '=' converts to '%3D'))

Once this is done, stick it in your request and hopefully you are good to go!

Pyrrho answered 3/7, 2012 at 13:21 Comment(5)
Hey I am facing problem in generating the signature. I am following the same method mentioned by you. Can you help me out in this. GET ecs.amazonaws.com /onca/xml AWSAccessKeyId=<Hidden>&AssociateTag=<Hidden>&Keywords=harry potter&Operation=ItemSearch&SearchIndex=Books&Service=AWSECommerceService&Timestamp=2012-07-27T11%3A12%3A46Z&Version=2011-08-01Paralysis
have you put the '/n' newline charecters in ok, in the same places i have?Pyrrho
I solved the problem and you answer helped me a lot... Thanks! +1 for thisParalysis
Just to add-on to this. To get the timestamp in there properly: NSDate *currentTime = [NSDate date]; NSString *timestamp = [currentTime stringWithISO8601Format]; NSString *timestampAmazon = [timestamp stringWithURLEncoding];Lukash
What is the equivalent of <AWSiOSSDK/AmazonAuthUtils.h> in today's AWSCore and related SDK? I don't see any function or framework that provides the same functionality.Becoming
D
8

Check out my Amazon Product Advertising Client https://github.com/m1entus/RWMAmazonProductAdvertisingManager

Some code with requesst serialization:

NSString * const RWMAmazonProductAdvertisingStandardRegion = @"webservices.amazon.com";
NSString * const RWMAmazonProductAdvertisingAWSAccessKey = @"AWSAccessKeyId";
NSString * const RWMAmazonProductAdvertisingTimestampKey = @"Timestamp";
NSString * const RWMAmazonProductAdvertisingSignatureKey = @"Signature";
NSString * const RWMAmazonProductAdvertisingVersionKey = @"Version";
NSString * const RWMAmazonProductAdvertisingCurrentVersion = @"2011-08-01";

NSData * RWMHMACSHA256EncodedDataFromStringWithKey(NSString *string, NSString *key) {
    NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding];
    CCHmacContext context;
    const char *keyCString = [key cStringUsingEncoding:NSASCIIStringEncoding];

    CCHmacInit(&context, kCCHmacAlgSHA256, keyCString, strlen(keyCString));
    CCHmacUpdate(&context, [data bytes], [data length]);

    unsigned char digestRaw[CC_SHA256_DIGEST_LENGTH];
    NSUInteger digestLength = CC_SHA256_DIGEST_LENGTH;

    CCHmacFinal(&context, digestRaw);

    return [NSData dataWithBytes:digestRaw length:digestLength];
}

NSString * RWMISO8601FormatStringFromDate(NSDate *date) {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
    [dateFormatter setDateFormat:@"YYYY-MM-dd'T'HH:mm:ss'Z'"];
    [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];

    return [dateFormatter stringFromDate:date];
}

NSString * RWMBase64EncodedStringFromData(NSData *data) {

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
    return [data base64EncodedStringWithOptions:0];
#else
    return [data base64Encoding];
#endif

}

//http://docs.aws.amazon.com/AWSECommerceService/latest/DG/rest-signature.html

- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError * __autoreleasing *)error
{
    NSParameterAssert(request);

    NSMutableURLRequest *mutableRequest = [request mutableCopy];

    if (self.accessKey && self.secret) {
        NSMutableDictionary *mutableParameters = [parameters mutableCopy];
        NSString *timestamp = RWMISO8601FormatStringFromDate([NSDate date]);

        if (!mutableParameters[RWMAmazonProductAdvertisingAWSAccessKey]) {
            [mutableParameters setObject:self.accessKey forKey:RWMAmazonProductAdvertisingAWSAccessKey];
        }
        mutableParameters[RWMAmazonProductAdvertisingVersionKey] = RWMAmazonProductAdvertisingCurrentVersion;
        mutableParameters[RWMAmazonProductAdvertisingTimestampKey] = timestamp;

        NSMutableArray *canonicalStringArray = [[NSMutableArray alloc] init];
        for (NSString *key in [[mutableParameters allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
            id value = [mutableParameters objectForKey:key];
            [canonicalStringArray addObject:[NSString stringWithFormat:@"%@=%@", key, value]];
        }
        NSString *canonicalString = [canonicalStringArray componentsJoinedByString:@"&"];
        canonicalString = CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                                    (__bridge CFStringRef)canonicalString,
                                                                                    NULL,
                                                                                    CFSTR(":,"),
                                                                                    kCFStringEncodingUTF8));

        NSString *method = [request HTTPMethod];

        NSString *signature = [NSString stringWithFormat:@"%@\n%@\n%@\n%@",method,self.region,self.formatPath,canonicalString];

        NSData *encodedSignatureData = RWMHMACSHA256EncodedDataFromStringWithKey(signature,self.secret);
        NSString *encodedSignatureString = RWMBase64EncodedStringFromData(encodedSignatureData);

        encodedSignatureString = CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                                           (__bridge CFStringRef)encodedSignatureString,
                                                                                           NULL,
                                                                                           CFSTR("+="),
                                                                                           kCFStringEncodingUTF8));

        canonicalString = [canonicalString stringByAppendingFormat:@"&%@=%@",RWMAmazonProductAdvertisingSignatureKey,encodedSignatureString];

        mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", canonicalString]];

    } else {
        if (error) {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"Access Key and Secret Required", @"RWMAmazonProductAdvertisingManager", nil)};
            *error = [[NSError alloc] initWithDomain:RWMAmazonProductAdvertisingManagerErrorDomain code:NSURLErrorUserAuthenticationRequired userInfo:userInfo];
        }
    }

    return mutableRequest;

}
Derisive answered 29/4, 2014 at 9:21 Comment(1)
I believe this answer would be better if it contained the gist of how it worked. Link-only answers are susceptible to link rot, after all.Penetrating
S
5

Thanks for all answers on this page. Here is what has worked for me (Swift 3.0):

Podfile:

pod 'AWSAPIGateway', '~> 2.4.7'

Swift code

static let kAmazonAccessID = "BLAH BLAH BLAH"
static let kAmazonAccessSecretKey = "BLAH BLAH BLAH"

static let kAmazonAssociateTag = "BLAH BLAH BLAH"
private let timestampFormatter: DateFormatter

init() {
    timestampFormatter = DateFormatter()
    timestampFormatter.timeZone = TimeZone(identifier: "GMT")
    timestampFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss'Z'"
    timestampFormatter.locale = Locale(identifier: "en_US_POSIX")
}

private func signedParametersForParameters(parameters: [String: String]) -> [String: String] {
    let sortedKeys = Array(parameters.keys).sorted(by: <)

    let query = sortedKeys.map { String(format: "%@=%@", $0, parameters[$0] ?? "") }.joined(separator: "&")

    let stringToSign = "GET\nwebservices.amazon.com\n/onca/xml\n\(query)"

    let dataToSign = stringToSign.data(using: String.Encoding.utf8)
    let signature = AWSSignatureSignerUtility.hmacSign(dataToSign, withKey: AmazonAPI.kAmazonAccessSecretKey, usingAlgorithm: UInt32(kCCHmacAlgSHA256))!

    var signedParams = parameters;
    signedParams["Signature"] = urlEncode(signature)

    return signedParams
}

public func urlEncode(_ input: String) -> String {
    let allowedCharacterSet = (CharacterSet(charactersIn: "!*'();:@&=+$,/?%#[] ").inverted)

    if let escapedString = input.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) {
        return escapedString
    }

    return ""
}

func send(url: String) -> String {
    guard let url = URL(string: url) else {
        print("Error! Invalid URL!") //Do something else
        return ""
    }

    let request = URLRequest(url: url)
    let semaphore = DispatchSemaphore(value: 0)

    var data: Data? = nil

    URLSession.shared.dataTask(with: request) { (responseData, _, _) -> Void in
        data = responseData
        semaphore.signal()
    }.resume()

    semaphore.wait(timeout: .distantFuture)

    let reply = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
    return reply
}

and here is the function that ask Amazon for price of a product:

public func getProductPrice(_ asin: AmazonStandardIdNumber) -> Double {

    let operationParams: [String: String] = [
        "Service": "AWSECommerceService",
        "Operation": "ItemLookup",
        "ResponseGroup": "Offers",
        "IdType": "ASIN",
        "ItemId": asin,
        "AWSAccessKeyId": urlEncode(AmazonAPI.kAmazonAccessID),
        "AssociateTag": urlEncode(AmazonAPI.kAmazonAssociateTag),
        "Timestamp": urlEncode(timestampFormatter.string(from: Date())),]

    let signedParams = signedParametersForParameters(parameters: operationParams)

    let query = signedParams.map { "\($0)=\($1)" }.joined(separator: "&")
    let url = "http://webservices.amazon.com/onca/xml?" + query

    let reply = send(url: url)

    // USE THE RESPONSE
}
Swimming answered 24/6, 2017 at 23:27 Comment(2)
you save me lots of timeJacquiline
hi, I'm using 'Keywords' parameter for ItemSearch. But it says the signatures do not match.Faria
D
4

A few steps missing from P-double post.

Prior to constructing the unsigned string, you will need to get the Timestamp value in place.

NSTimeZone *zone = [NSTimeZone defaultTimeZone];                //get the current application default time zone
NSInteger interval = [zone secondsFromGMTForDate:[NSDate date]];//sec Returns the time difference of the current application with the world standard time (Green Venice time)

NSDate *nowDate = [NSDate dateWithTimeIntervalSinceNow:interval];
NSDateFormatter * formatter = [[NSDateFormatter alloc] init];

[formatter setTimeZone:[NSTimeZone systemTimeZone]];// get current date/time
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];

// display in 12HR/24HR (i.e. 11:25PM or 23:25) format according to User Settings
[dateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
NSString *currentTime = [dateFormatter stringFromDate:nowDate];

NSString* encodedTime = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef) currentTime,NULL, CFSTR("!*'();:@&=+$,/?%#[]"),kCFStringEncodingUTF8));

NSString* unsignedString = [NSString stringWithFormat:@"GET\nwebservices.amazon.com\n/onca/xml\nAWSAccessKeyId=AKIAI443QEMWI6KW55QQ&AssociateTag=sajjmanz-20&Condition=All&IdType=ASIN&ItemId=3492264077&Operation=ItemLookup&ResponseGroup=Images%%2CItemAttributes%%2COffers&Service=AWSECommerceService&Timestamp=%@&Version=2011-08-01", encodedTime];

Once the date is url friendly encoded, the rest of the steps works like a charm.

Lastly, I also used CFURLCreateStringByAddingPercentEscapes listed above to encode the string generated by the AmazonAuthUtils HMACSign message call.

Dugald answered 21/2, 2013 at 10:6 Comment(0)
K
4

Am I wrong that it is not 'legal' (according to Amazon's own guidelines) to use Amazon's Product Advertising API in an iOS app without Amazon's express written consent?

Kamat answered 14/3, 2013 at 3:14 Comment(4)
maybe he's in california, where certain provisions of clickwrap agreements have been invalidated.Uball
I would love to be able to use this api. Do you think it will ever become open to iOS in general?Kamat
Don't know much about it. One thing I can tell you for sure -- many many developers will never think to read those guidelines and are using it in their apps, unless there are other technical blockages or the app store is rejecting them.Uball
I just think Amazon would benefit so much from allowing the use of the API in iOS apps, and allowing the affiliate program as well (which of course would also benefit developers.) I am hoping that the 'ban' is lifted soon.Kamat
M
4

Swift 2.0

Here is a function that will sign a set of parameters for Swift. Note that this code requires the Alamofire and AWSCore Cocoapods to be installed. You also need to add #import <CommonCrypto/CommonCrypto.h> to your Objective-C Bridging header otherwise kCCHmacAlgSHA256 won't be found.

private func signedParametersForParameters(parameters: [String: String]) -> [String: String] {
    let sortedKeys = Array(parameters.keys).sort(<)

    var components: [(String, String)] = []
    for key in sortedKeys {
        components += ParameterEncoding.URLEncodedInURL.queryComponents(key, parameters[key]!)
    }

    let query = (components.map { "\($0)=\($1)" } as [String]).joinWithSeparator("&")

    let stringToSign = "GET\nwebservices.amazon.com\n/onca/xml\n\(query)"
    let dataToSign = stringToSign.dataUsingEncoding(NSUTF8StringEncoding)
    let signature = AWSSignatureSignerUtility.HMACSign(dataToSign, withKey: kAmazonAccessSecretKey, usingAlgorithm: UInt32(kCCHmacAlgSHA256))!

    let signedParams = parameters + ["Signature": signature]

    return signedParams
}

It's called like this:

let operationParams: [String: String] = ["Service": "AWSECommerceService", "Operation": "ItemLookup", "ItemId": "045242127733", "IdType": "UPC", "ResponseGroup": "Images,ItemAttributes", "SearchIndex": "All"]
let keyParams = ["AWSAccessKeyId": kAmazonAccessID, "AssociateTag": kAmazonAssociateTag, "Timestamp": timestampFormatter.stringFromDate(NSDate())]
let fullParams = operationParams + keyParams

let signedParams = signedParametersForParameters(fullParams)

Alamofire.request(.GET, "http://webservices.amazon.com/onca/xml", parameters: signedParams).responseString { (response) in
    print("Success: \(response.result.isSuccess)")
    print("Response String: \(response.result.value)")
}

Finally, the timestampFormatter is declared like this:

private let timestampFormatter: NSDateFormatter

init() {
    timestampFormatter = NSDateFormatter()
    timestampFormatter.dateFormat = AWSDateISO8601DateFormat3
    timestampFormatter.timeZone = NSTimeZone(name: "GMT")
    timestampFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
}

You can use/modify to suit your needs, but everything that's necessary should be there.

Madelon answered 12/10, 2015 at 15:29 Comment(4)
This seems to be the best answer to what I've seen, but it's still giving me an error : ...<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method... I've been trying to figure this out for days, any help would be appreciated. :)Griner
Awsome. Do you have this code in Objective-C available by any chance? Looks great.Kamat
Afraid I don't have it for ObjC, mostly because of the Alamofire stuff it uses which I think is Swift only. Perhaps some of the top answers could help for ObjC? It's been a while since I was last looking at this.Madelon
@CoryImdieke In "ResponseGroup": "Images,ItemAttributes" if its two value like Images, attributes then it says The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method or if its responsegroup value is one like "images" then i can able get data..what was the issue. i could not able make it.Arpeggio
C
3

Instead of using CommonCrypto, which is deprecated in modern OS X, you can also use SecTransforms:

CFErrorRef error = NULL;

SecTransformRef digestRef = SecDigestTransformCreate(kSecDigestHMACSHA2, 256, &error);
SecTransformSetAttribute(digestRef, kSecTransformInputAttributeName, (__bridge CFDataRef)self, &error);
SecTransformSetAttribute(digestRef, kSecDigestHMACKeyAttribute, (__bridge CFDataRef)key, &error);

CFDataRef resultData = SecTransformExecute(digestRef, &error);
NSData* hashData = (__bridge NSData*)resultData;

CFRelease(digestRef);
Coimbra answered 22/1, 2015 at 13:1 Comment(1)
I don't believe this will work on iOS though, where SecDigestTransformCreate() doesn't exist (as of iOS 8.0).Artefact

© 2022 - 2024 — McMap. All rights reserved.