Invalid transaction receipt returned by appStoreReceiptURL (NSData), in iOS 7

Mustafa picture Mustafa · Nov 13, 2013 · Viewed 8.4k times · Source

I'm using following method to get the receipt data:

// Use this method instead of accessing transaction.transactionReceipt directly!
- (NSData *)appStoreReceiptForPaymentTransaction:(SKPaymentTransaction *)transaction {
    NSData *receiptData = nil;

    // This is just a quick/dummy implementation!
    if (kiOS7) {
        NSURL *receiptFileURL = [[NSBundle mainBundle] appStoreReceiptURL];
        receiptData = [NSData dataWithContentsOfURL:receiptFileURL]; // Returns valid NSData object

    } else {
        receiptData = transaction.transactionReceipt; // Returns valid NSData object
    }        

    return receiptData;
}

I'm using following code to read the JSON receipt:

NSData *receipt = [self appStoreReceiptForPaymentTransaction:transaction];

NSError *error = nil;
NSDictionary *receiptDict = [receipt dictionaryFromPlistData:&error];
NSString *transactionPurchaseInfo = [receiptDict objectForKey:@"purchase-info"];
NSString *decodedPurchaseInfo = [NSString stringWithUTF8String:[[NSData dataFromBase64String:transactionPurchaseInfo] bytes]];
NSDictionary *purchaseInfoDict = [[decodedPurchaseInfo dataUsingEncoding:NSUTF8StringEncoding] dictionaryFromPlistData:&error];

NSString *transactionID = [purchaseInfoDict objectForKey:@"transaction-id"];
NSString *purchaseDateString = [purchaseInfoDict objectForKey:@"purchase-date"];
NSString *signature = [receiptDict objectForKey:@"signature"];
NSString *signatureDecoded = [NSString stringWithUTF8String:[[NSData dataFromBase64String:signature] bytes]];

// Convert the string into a date
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss z"];

NSDate *purchaseDate = [dateFormat dateFromString:[purchaseDateString stringByReplacingOccurrencesOfString:@"Etc/" withString:@""]];

NSLog(@"Raw receipt content: \n%@", [NSString stringWithUTF8String:[receipt bytes]]);
NSLog(@"Purchase Info: %@", purchaseInfoDict);
NSLog(@"Transaction ID: %@", transactionID);
NSLog(@"Purchase Date: %@", purchaseDate);
NSLog(@"Signature: %@", signatureDecoded);

The dictionaryFromPlistData: method returns proper NSDictionary object for data returned by transactionReceipt, but it returns nil using data returned using appStoreReceiptURL -- and consequetly I don't get valid receipt!

- (NSDictionary *)dictionaryFromPlistData:(NSError **)outError {
    NSError *error;
    NSDictionary *dictionaryParsed = [NSPropertyListSerialization propertyListWithData:self
                                                                               options:NSPropertyListImmutable
                                                                                format:nil
                                                                                 error:&error];

    if (!dictionaryParsed) {

        if (error) {
            *outError = error;
        }

        return nil;
    }

    return dictionaryParsed;
}

Why isn't the above code working in iOS 7?

Just to reiterate, dictionaryFromPlistData: correctly converts the NSData returned by transaction.transactionReceipt.

Answer

capikaw picture capikaw · Dec 18, 2013

iOS 7 has changed how receipt validation is done. I went through the exact path you're down and got the same results. What I found was that the receipt from [[NSBundle mainBundle] appStoreReceiptURL] requires a different method for decoding than the previous transaction.transactionReceipt.

Here is a snippet from Apple's docs:

The outermost portion is a PKCS #7 container, as defined by RFC 2315, with its payload encoded using ASN.1 (Abstract Syntax Notation One), as defined by ITU-T X.690. The payload is composed of a set of receipt attributes. Each receipt attribute contains a type, a version, and a value.

If you're looking for a quick fix, I've seen success here: https://github.com/rmaddy/VerifyStoreReceiptiOS