One of the big advantages of in-app purchases for some people is that it makes pirating content more difficult. For me it wasn’t much of an issue because a) Most of the people who enjoy Flower Garden aren’t the ones checking out the app warez sites, and b) I know better than to waste my time trying to prevent piracy [1]. Besides, I always roll my eyes whenever I hear that 90% or some other large percentage of the users are using a pirated copy, so the developer lost all that money. News flash: 99% of those users using a cracked version wouldn’t have bought your product anyway, so you can rest a bit easier at night.
Even so, I decided to take a few extra steps that at least will make things a bit more difficult to crack and keep things more secure. I could have gone way beyond this, but I don’t think that would have been worth my time.
Verify the purchase
Right now, you can download programs that will automatically crack an app by extracting its data and resigning it so it works on any jailbroken phone. There’s no doubt that even if you implement in-app purchases just like I described in earlier posts, crackers will have to analyze the code and do some steps by hand, so that might be enough to slow down the pace of cracked apps. Even so, an experienced cracker won’t take long to crack your in-app purchases unless you take some extra steps. All they have to do is find the function that you call in response to getting the go ahead from the App Store and they’ll gain access to all content.
The first thing you should do is to verify that the purchase happened correctly. To do that, once you get the notification in your app that the payment was complete, you send the receipt to your server, and have your server verify it in turn with the App Store. Since crackers don’t have access to your server (if they do you’re in more trouble than a few cracked apps), that step is secure. If your app was cracked and the user didn’t pay for the purchase, the App Store will fail the verification of the receipt and you don’t have to deliver the content.
How do you check that the receipt is valid? First of all, you need to send the receipt of the purchase to your server. I send it along with some other info, like the user’s UDID, what product was purchased, etc so I can keep general usage statistics. The relevant bits of code are the following:
NSString* receiptString = [self createEncodedString:transaction.transactionReceipt]; //... [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[@"Content-Disposition: form-data; name=\"receipt\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[receiptString dataUsingEncoding:NSUTF8StringEncoding]]; //... [req setHTTPBody:postBody];
The createEncodedString function takes some arbitrary data and creates a base64 encoding. That’s one of the parts that is not particularly well documented in the SDK documentation.
- (NSString*) createEncodedString:(NSData*)data
{
static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const int size = ((data.length + 2)/3)*4;
uint8_t output[size];
const uint8_t* input = (const uint8_t*)[data bytes];
for (int i = 0; i < data.length; i += 3)
{
int value = 0;
for (int j = i; j < (i + 3); j++)
{
value <<= 8;
if (j < data.length)
value |= (0xFF & input[j]);
}
const int index = (i / 3) * 4;
output[index + 0] = table[(value >> 18) & 0x3F];
output[index + 1] = table[(value >> 12) & 0x3F];
output[index + 2] = (i + 1) < data.length ? table[(value >> 6) & 0x3F] : '=';
output[index + 3] = (i + 2) < data.length ? table[(value >> 0) & 0x3F] : '=';
}
return [[NSString alloc] initWithBytes:output length:size encoding:NSASCIIStringEncoding];
}
That just sends the receipt to my server. Once the server receives that request, it needs to verify the data with the App Store. The exact format of the App Store request is, again, not well documented. It turns out it needs to be sent as anonymous data in a POST request. After some trial and error, this is the php code I’m using in my server to verify the receipt:
function isReceiptValid($purchase)
{
$data = array ('receipt-data' => $purchase->receipt );
$encodedData = json_encode($data);
$url = "https://buy.itunes.apple.com/verifyReceipt";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedData);
$encodedResponse = curl_exec($ch);
curl_close($ch);
if ($encodedResponse == false)
{
logError($purchase, "Payment could not be verified (no response data).");
return false;
}
$response = json_decode($encodedResponse);
if ($response->{'status'} != 0)
{
logError($purchase, "Payment could not be verified (status != 0).");
return false;
}
$purchase->storeReceipt = $response->{'receipt'};
return true;
}
If things check out, the server returns a valid code, and you can go ahead and make the content available to the user. Otherwise, it means the user is trying to pass up a fake purchase as valid and you can deal with it any way you want.
Get content from the server
Of course, if the content is already embedded in the app itself, the cracker can bypass both the App Store purchase and the receipt verification through your server, and just access the content directly. If you want a better chance of not being cracked, have your server deliver the content. That way, if the receipt doesn’t check out, you never transfer the content.
In the case of Flower Garden, all seed packets are downloaded from the server, but the other purchases (extra pots, extra fertilizer) are just changing a few internal variables, so they would be a lot easier to crack.
One of the easiest ways that crackers can get around this verification is by buying the content once and saving the receipt that comes with it. Then, cracked versions can pass that receipt to your server, they’ll be validated as correctly, and they’ll be able to download the content. So, as an added precaution, you might want to keep track how many times a certain purchase is re-downloaded, especially from different UDIDs. I can’t imagine a legitimate reason for a user to download the same purchase more than 20-30 times from different devices, so denying the purchase at that point in the server seems justified.
Save those receipts
The last step I to make cracking more difficult was to save the receipt itself to disk along with the content that was downloaded. Then, periodically, the game can pick one of those receipts, and verify with the server that it’s still valid. If it’s ever reported as an invalid receipt, the game deletes the content for that purchase. This is not as draconian as it sounds, because even if it’s ever triggered for a legitimate user (and I have no idea how that could be other than data corruption), they can always re-purchase it again for free (and they would want to do that to fix their data corruption anyway).
To be able to delete things easily and cleanly, I keep each downloaded in-app purchase on a separate directory. So it’s very easy to delete the whole directory without having to access files everywhere. The hardest thing is making sure the game deals with the missing data and that nothing is referencing it. I’ll touch on that on a future part of this article.
Keeping purchases in separate directories also makes it easier to reset all purchases. That’s something I didn’t originally include in Flower Garden, but I should have. Sometimes, data can get corrupted or maybe a bug occurs because of some combination of purchases (hopefully not, but you never know!). Allowing the user to reset the purchases and re-download them allows them to fix those problems without having to reinstall the app and lose their progress.
[1] I know from first hand experience that one dedicated teenager with all the time in his hands can spend inordinate amounts of time bypassing the most elaborate anti-piracy schemes. The harder it is, the more of a challenge it becomes, and the more fun it is. To this day, I still remember the thrill of finally bypassing the copy protection of 1943 on the Amstrad CPC after two days of non-stop hacking, which included several loops XORing all the available memory (including the currently executing code), and messages from the developers taunting me!