Is my in app purchase receipt validation process correct? What do I do once validity is established?

I'm looking to add receipt validation to the in app purchase in my app. The in app purchase basically just unlocks a "pro" version of the app that provides the users with some extra features, such as a dark mode. I'm reading a lot about the in app purchase receipt validation flow, and it's a lot to take in.


Am I doing this right? When the user makes their purchase, I send the resulting receipt off to my server, and validate it with Apple. If Apple "okays" the receipt, and it matches the bundle ID for my app, then I return "true" back to my app from my server, and now my app knows whether the receipt is valid.


1. Is this flow correct? What's stopping them from faking my server's "true" response?

2. What do I do now that I know that receipt is valid? I obviously don't set a flag in NSUserDefaults (they could tamper), so how do I know fifteen minutes later when the user tries to use dark mode that they are a paid user with a valid receipt? Just check for the presence of a receipt, and if so, how do I know that the receipt found is valid, surely I don't check server-side for validity every time the feature is accessed? What about at app launch, should I check every time there? What if I check at launch, see no receipt, infer that they're not a "pro" user, and then five seconds later a jailbreak tweak attaches a fake receipt, the user tries to enable the mode, I see a receipt, and just give it to them?


I'm really mostly confused/concerned about number 2? Basically, after establishing a receipt is valid and they're a pro user, how do I keep that information?

>1. Is this flow correct? What's stopping them from faking my server's "true" response?

The flow is correct. The communication with between your device and your server needs to be protected. You can do that by sending the receipt from your app to your server together with the device's identifierForVendor (or some other device-specific identifier). Then have your server send back to the device a hash of the identifierForVendor along with an 'is valid' or 'is not valid' marker. Have the app check the hash to be sure that the server is responding to the device and that the message is correct. (This is essentially 'signing' the response.) This prevents a man-in-the-middle from inserting a false "is valid" code into the return signal to the device because they have no way of generating the hash of a unique identifierForVendor and an 'is valid'.


You also need to keep a list of transaction_ids on your server so someone doesn't insert a copy of an encoded receipt into a device and get you to accept it.


>2. I obviously don't set a flag in NSUserDefaults

You could load the NSUserDefaults with the identifierForVendor or, better, a hash of the identifierForVendor.


Here is some useful code:



#import <CommonCrypto/CommonDigest.h>
            NSError *error
            NSData *data =[NSPropertyListSerialization dataWithPropertyList:
                  [NSArray arrayWithObjects:[[[UIDevice currentDevice] identifierForVendor] UUIDString],
                               @"my very secret string here",nil]
                   format:NSPropertyListXMLFormat_v1_0 options:0 error:&error ];
            unsigned char result[CC_SHA1_DIGEST_LENGTH];
            CC_SHA1([data bytes], (unsigned int)[data length], result);
            fileHash = [NSString  stringWithFormat:
                        @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
                        result[0], result[1], result[2], result[3], result[4],
                        result[5], result[6], result[7],
                        result[8], result[9], result[10], result[11], result[12],
                        result[13], result[14], result[15],
                        result[16], result[17], result[18], result[19]
                        ];

Hey, thanks for the response, I was hoping you'd be able to chime in, I've seen your name a lot around these parts. 🙂


Curious, as someone who's not the best crytographer, why hash the identifierForVendor? So I send it to the server unhashed, hash it on the server, then send it back hashed, and then locally hash what I sent (with the same hash algorithm as on the server) and check that against what the server sent back? Why not just not hash it at all?


For your suggestion of keeping a list of transaction_ids, I assume that is to stop someone from buying the in-app purchase once, and then giving that "valid" receipt around for everyone to use (with say a jailbreak)? If so, how do I differentiate between the transaction_id being reused because it's from an in-app purchase "restore" (a valid case) versus being stolen and reused (someone being evil)?


Lastly, could you explain how the NSUserDefaults vendorIdentifier hash would work as a "pro" flag? What's stopping a user from figuring out my hashing algorithm, and then just hashing their own vendorIdentifier into NSUserDefaults? Again, I'm somewhat crytographically challenged, but is your "my very secret string here" effectively a "salt" for the hash that they would have to know? And could they not decompile the app and figure out what that secret is, or is the fact that it's "inlined" helping here?


Also, should I be revalidating at launch or at some point?


Again, thank you so much!

If someone places a man-in-the-middle of the communication between the device and your server they can replace the 'not valid' with 'valid' so you need to have a unique and proprietary 'valid' response for each device. You make it unique with the identifierForVendor which is unique but known by the man-in-the-middle. You make it proprietary by sending a salted hash. You can send it unhashed and include a signature to prove it is coming from the server. A signature would be a hash of the identifierForVendor combined with the 'valid' field and a salt. ....The salt prevents someone from figuring out your algorithm. I do not believe (but I don't have expertise in this area) that someone can decompile your code and figure out how to crack your app. Perhaps you should use a word like 'hotdog' rather than 'is valid'....,,you are correct about a transaction Id no longer being unique. This is a serious security violation by Apple and My error in my earlier post. Use the requirement that the receipt request date come after the payment request date and use the payment request date rather than the identifierForVendor in the communication with your server. ..,and thanks for the complement.

Awesome, thanks for that man in the middle explanation, that clears things up a lot. Complement well deserved.


For the uniqueness of the transaction_id, could you explain what you mean by requiring that the receipt request data come after the payment request date? How does that enforce uniqueness? Wouldn't it always come after (date-wise)? Or do you use that in conjunction with the transaction_id? Could you elaborate a little more by chance?


And then why would I use the payment request date rather than identifierForVendor as the "sign" for the server, what's wrong with identifierForVendor?


My plan effectively as it stands now (thanks to your advice) is to use the transaction_id and the receipt request date to verify uniqueness server-side (I'll store both) and then the vendorIdentifier as the hash. Would that not work?

That will work fine. The reason for using the receipt request date is because, as you point out, the transaction_id is not unique because it is duplicated in a purchase and a restore (and a repurchase). So the challenge becomes, how does your server spot a receipt that was generated by one user and copied and handed to other users?


Your idea of requiring that the receipt request date (actually, the creation_date) combined with the transaction_id be a unique pair is pretty good but not fool proof. A hacker can generate multiple valid receipts by hitting restore over and over again. Then, instead of sending the resulting receipts to your server, sending each duplicate receipt to a friend. Then the friend can install the receipt and make a call to updatedTransactions thereby faking a purchase that submits the receipt to your server. Your server will accept the receipt (it's valid) and give the friend's device credit for the IAP. (I just realized - in the old days where a restore came with a new transaction_id - you could do this same hack.) (Also - you should know that if you decode the receipt yourself you can verify that the receipt is signed with the identifierForVendor of the specific device. That is why decoding yourself is better, but harder, than using the Apple servers.)

So my suggestion was to accept any receipt whose creation_date is after the date that the app generated the [[SKPaymentQueue defaultQueue] addPayment: (the payment request date) as it must be. That way a hacker would need to generate the receipt contemporaneous with its submission for verification - and that would be hard. (Note - this system fails in the ask-to-buy situation where there can be a considerable delay between the payment request date, the creation_date and the submission for verification - I don't know how to handle that - I decode onboard). On reflection, I like your approach better.

The identifierForVendor is just being used as a random number so that the valid signature coming from your app is different from the valid signature coming from another app. That prevents a hacker from generating the package from one device and grabbing it and handing it to another device for submission from that other device. Any random number will do. Since the scheme above requires sending the payement request date, and since that is sufficiently 'random' and makes each signature unique, it can be used in place of the identifierForVendor.

Is my in app purchase receipt validation process correct? What do I do once validity is established?
 
 
Q