Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Base64 encoding weirdness for iOS7 in-app purchase receipt server verification

I've found some very odd issues with base64 encoding of iOS7 receipts.

At the moment (Xcode5/iOS7) have 2 methods for getting a receipt for an in-app purchase:

  1. Deprecated method that returns a single receipt. [SKPaymentTransaction transactionReceipt]
  2. A bundle of all receipts from location appStoreReceiptURL

My App sells web site based services using a credit system which makes server receipt validation necessary. The App also sells downloadable extensions. So a mixture of consumables and non-consumables. The non-consumables are downloaded from Apple so no verification required. The consumables are packages of credits used for purchasing services on the website.

When iOS7 & XCode5 launched I updated my App but struggled with the new bundled receipt located at appStoreReceiptURL. With the new style, all receipts bundled in one, my server got back this error from Apple's verifyReceipt sandbox

[status] => 21002
[exception] => java.lang.IllegalArgumentException

I gave up after seening posts on stackoverflow saying others had experienced the same issue and that the opinion at the time was it was a bug, or yet to be added feature, on Apple's part. However I've been seeing issues with using the old deprecated method which has forced me back to trying again. After a lot of debugging and searching I finally worked out some of what was going wrong - as well as getting it working but in a very ugly way which I'm not confident with going live with.

I would have thought that NSData (plain byte buffer) encoded output wouldn't differ much. Here's the weird part I'm seeing.

The original deprecated single receipts can be base64 encode either as 64 character lines with \r\n on the end or without. Apple verification server doesn't care. It's happy either way.

For the new receipt bundle it won't work unless 2 things are done. First the base64 encoding cannot have line breaks. I notice Apple has added it's own base64 encoder into iOS7. This is what I'm using to get an encoded output that works for both receipt types.

NSString *receiptDataString = [transactionReceipt base64EncodedStringWithOptions:0];

The 2nd thing that needs to be done is to search and replace all space characters from the received encoded receipt bundle on the server. i.e.

$receiptdata = str_replace(' ', '+', $receiptdata);

I happened to noticed this was a difference between the receipts being received by my server. Why I don't know? I'm using AFNetworking-1.3.2's JSONRequestOperationWithRequest

    NSMutableArray *pairs = [[NSMutableArray alloc] initWithCapacity:0];
    for (NSString *key in parameters) {
        [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, [parameters objectForKey:key]]];
    }
    postData = [[pairs componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
    request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://myserver.com/index.php"]];
    [request setHTTPMethod:@"POST"];
    [request setValue:[NSString stringWithFormat:@"%d", postData.length] forHTTPHeaderField:@"Content-Length"];
    [request setValue:@"application/x-www-form-urlencoded charset=utf-8" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:postData];
    operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request

Chris Prince claims to have it working with AFNetworking 2 in his reply to this question

Why could these two NSData receipts cause encoding/decoding problems like I'm seeing? I've been as detailed and clear as I can be here to help others as I see there's a lot of folks feeling the same pain here with this new receipt method Apple's introduced.

like image 811
Seoras Avatar asked Jan 01 '14 14:01

Seoras


1 Answers

The answer turned out to be due to

stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding

not escaping all special characters like :/?#[]@!$&’()*+,;= etc. The new receipt Apple provides, once base64 encoded, contains characters which need escaping but are not escaped by the above iOS lib escape encoder. Looking around I found one on here which works well. See below for working code. (Server doesn't need to parse receipt code now and can use it straight out of $_POST)

// Lifted from:
// http://stackoverflow.com/questions/2159341/nsstring-method-to-percent-escape-for-url
//
- (NSString *)urlEncodeValue:(NSString *)str {
    NSString *result = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR(":/?#[]@!$&’()*+,;="), kCFStringEncodingUTF8));
    return result;
}

    // Encode and pair basic parameters
    NSMutableArray *pairs = [[NSMutableArray alloc] initWithCapacity:0];
    NSString *part;
    for (NSString *key in parameters) {
        NSString *encodedValue = [[parameters objectForKey:key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSString *encodedKey = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        part = [NSString stringWithFormat: @"%@=%@", encodedKey, encodedValue];
        [pairs addObject:part];
        [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, [parameters objectForKey:key]]];
    }

    // Receipt encoded and paired separately
    receiptDataString = [self urlEncodeValue:receiptDataString];
    part = [NSString stringWithFormat: @"receipt=%@", receiptDataString];
    [pairs addObject:part];

    // Post data.
    postData = [[pairs componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
like image 172
Seoras Avatar answered Sep 20 '22 17:09

Seoras