Apple’s App Store launched in 2008 as a marketplace for distributing free or paid apps for their iOS platform, and the next year they introduced in-app purchases — a way for app developers to sell digital goods or additional functionality from within their apps. Initially this was only available for paid apps, but quickly thereafter Apple enabled them for free apps as well. Today, offering the main app for free and making money via in-app purchases is a very common approach, and seems to work well for many.
As with any feature that has anything to do with (real-life) money, security is very important when implementing in-app purchases. Specifically, it’d be good to ensure that when you’re providing a user with a paid resource, they have actually paid for it. Mobile platform system APIs do their best to make it as easy as possible for app developers to do this, but there is still lots of nontrivial design and implementation work to do.
In this article we will go through the basic process of performing in-app purchase payments, and discuss approaches for ensuring that paid resources are only provided for users who have actually paid for them.
This has been written with iOS in mind, but the general principles should hold also for Android and Windows, given that the in-app purchase systems for these three platforms are very similar.
The purchase flow
Apple provides the StoreKit framework in the iOS SDK for working with in-app purchases. When your app is ready to initiate a purchase, it creates an SKPayment object and adds it to the SKPaymentQueue, which makes the system throw a dialog up on the screen to let the user know that payment is being requested:
After the user has confirmed the purchase, the iOS device sends a purchase transaction request to Apple’s servers.
When Apple has processed the purchase on their end (mainly: successfully charged the purchase amount from the user’s credit card) they will send a response to the device indicating this. This response contains a receipt for the transaction — a signed document specifying what was purchased, by whom, in what app, when the purchase took place, etc.
Your app is now responsible for the following steps:
- Inspect the receipt and verify that it identifies a valid purchase transaction
- Fulfill the purchase (give the user what they paid for)
- Tell iOS that the transaction has been handled (by marking it as “finished” using the SKPaymentQueue API)
Step #1 is where you have to be careful.
When inspecting an in-app purchase receipt, you primarily want to check two things:
- Authenticity — that the receipt comes from Apple and not someone else
- Integrity — that the receipt has not been tampered with
In addition to these basic checks, you’ll also want to make sure that the receipt is for the correct product in the correct app, and that it hasn’t been used before. We’ll get back to these a bit later.
There are two ways to perform the basic authenticity and integrity checks: manually, or via Apple’s HTTP validation service. The Receipt Validation Guide in Apple’s developer documentation describes both of these.
The objc.io periodical has a good article on implementing manual receipt validation — this requires use of the OpenSSL APIs, and is not trivial.
The HTTP validation service, on the other hand, is much simpler to use: just send the receipt data to Apple’s server, and it’ll tell you whether the receipt is valid, and if so, provide a JSON document that can be easily read for further information. This option, however enticing, should never be used in the iOS app itself; it is strictly meant for server-side receipt validation (that is, validation performed on a server that you control.) This is what Apple’s engineers have to say about this in the WWDC 2013 session titled “Using Receipts to Protect Your Digital Sales”:
…but this is only to be used for your server to talk to our server to validate the receipt. It's not to be used for your app to talk directly to the validation service. So if you're doing that today, you really need to stop, because you can now validate the receipt on the device in itself.
The reason for this rule is that the connection between the user’s iOS device and Apple’s servers cannot be trusted: if your app were to validate receipts by asking Apple’s servers, that connection would be subject to man-in-the-middle attacks, which we’ll discuss next.
Man-in-the-middle (MITM) attacks
Jailbreaking is a term that refers to the bypassing of built-in security mechanisms in the iOS operating system for the purpose of gaining more control over the system. Many people do this willingly in order to avoid many of the restrictions Apple builds into the OS. Once an iOS device has been jailbroken, software running on it pretty much has free rein over the whole system, and is not limited by any of the security features that Apple has implemented.
People like free stuff, and some are willing to compromise their morals to get free stuff, so it comes as no surprise that there is readily available software that allows one to essentially “get free in-app purchases in any app”. This software works by intercepting the requests the device sends to Apple’s servers, and instead of letting the requests actually go through to Apple, responding with a fraudulent receipt. This is called a man-in-the-middle attack:
After an attack like this has taken place, your unsuspecting app will receive whatever (fraudulent) purchase receipt the MITM program has provided.
If the receipt were some random garbage file and your app didn’t bother to validate it at all, the user would just get their free stuff. But of course it’s not that simple because your app validates the receipt — right?
Well, validating the authenticity and integrity of the receipt is not enough: it’s easy enough for the developers of these MITM programs to capture some random valid in-app purchase receipt, store it in their program, and have it provide that same receipt over and over again for all the apps on the device whenever they try to initiate purchases. Since the receipt is completely valid and has actually been issued by Apple in the past, any validity and integrity checks performed on it (whether manually via the OpenSSL APIs or by asking Apple’s validation server) will pass.
A common receipt returned by one of these MITM programs has the product identifier com.zeptolab.ctrbonus.superpower1. This is apparently a valid receipt that Apple has at some point issued for the iOS game “Cut the Rope”.
The next step, then, is to check that the receipt is for the correct product in your app (by comparing the product identifier and bundle identifier in the receipt with your expected values). This check would thwart the attack described above.
Even this is not enough, though: these MITM programs could quite easily return valid receipts that have in fact been provisioned for the correct product in your app. They can do this by letting the first such payment transaction go through to Apple (requiring the user to actually pay, once), capturing the receipt in Apple’s response, storing it, and then again intercepting all future payment requests for that product, and providing the same captured receipt data over and over again.
In order to thwart these kinds of “replay attacks,” you can store the identifiers of all in-app purchase receipts as they are used and check each time that newly provisioned receipts have not been seen before.
You write your iOS app and submit it to the store, and then the rest is — in a sense — out of your hands. Users can download your app, break it open, modify it however they like (“crack” it,) and run the modified version on their jailbroken devices. This can be done by some individual who is specifically interested enough in your app to crack it, or it can even be done (to some extent) by some automatic process running on a jailbroken device.
When your app has been cracked, all bets are off: any security measures you’ve built into it can be overridden — it’s just a matter of how much resources you’re willing to invest in “hardening” the app (in order to make the cracker’s job slower and more difficult.)
This is not news to software engineers: it should be common knowledge that when it comes to security in client-server systems, you can never trust the client. This tenet, however, provides an important premise for the rest of our discussion.
Where we stand
Okay, let’s recap:
- If you validate the receipts in the client app, the app can be cracked and the validation bypassed.
- If you validate the receipts on your server, the connection from your client to your server can be intercepted and the response replaced with a fraudulent “yes, looks valid.” Or, the client app can be cracked and the validation bypassed.
The question then becomes: how does your app provide the purchased content to the user? Or more precisely: how does your system provide the purchased content to the client app?
The nature of the content for sale
The security of your in-app purchase implementation ultimately hinges on the nature of the content you’re selling.
If the content is naturally part of the client app and the user just pays to have it unlocked, then it’ll always be possible for the app to be cracked and modified to unlock the content without payment. This is typically the case with products like “100 gold coins” or “an unlocked game character”.
On the other hand, if the content is stored server-side and sent to the client in exchange for a valid purchase receipt, then:
- Cracking the client app is not useful: the receipt validation isn’t performed in the client, and the content for sale isn’t even there (it’s on the server)
- MITM attacks on the client-server connection are not useful: in order to legitimately stand in for the server, the MITM process would have to provide the purchased content in its response (which is exactly the thing it’s trying to get in the first place)
- MITM attacks on the connection between the device and Apple’s servers are not useful: the receipt validation performed by your server cannot be influenced by the attacker, so the receipt would have to be completely new (not seen before by your server) and completely valid for this attack to work. Notwithstanding unknown security vulnerabilities in Apple’s receipt signing system, we can reasonably safely assert that it’s not possible for an attacker to create such receipts without making a real purchase.
This distinction is the most important issue to discuss when you’re designing a system for providing in-app purchase products for sale in an iOS app: you should always strive for a solution where the server holds the content that the user wants to buy (whatever it is — hopefully not something that’s easy to replicate) and then only provides it in exchange for a fresh, valid receipt.
If the nature of the product you want to sell is such that the above proposition is not possible, and you must include the product in the client app itself, then you must accept the fact that it’ll always be possible for a determined attacker to crack your app in order to avoid paying. When implementing a system like this, just remember the following:
- When validating the receipts on the client, in addition to validity and integrity checks, also remember to compare the product ID and app bundle ID (in order to catch receipts provisioned for some other app)
- Store a list of redeemed receipt identifiers somewhere in the client (in order to catch trivial replay attacks)
- Spend some time — whatever is reasonable in your case — hardening your receipt validation code (in order to thwart trivial cracking attempts.) The “Secure Practices” section in the objc.io article on receipt validation contains some practical advice for this.
Of course, it’s also perfectly reasonable to skip many of the checks suggested here, implement some kind of “bare minimum” receipt validation, and just assume that the level of piracy will be low enough to not matter. However, a decision like that needs to be informed, and you must understand the consequences.