Version: 5.3 (switch to 5.4b)
Purchase Receipts
Store Extensions

Receipt validation

You can use receipt validation to make it harder for hackers to access content without paying for it.

Where to validate

Where you put validation logic depends on your Application’s requirements, but a general rule is that receipt validation should be performed at the point where content is dispensed.

For example, if you are selling content that is already bundled within your Application, your Application should contain all the logic necessary to decide if a player is entitled to it.

If, however, you are selling content that is delivered by a server, such as downloadable content or multiplayer features, that server is the natural place to apply validation.

Local validation

In this scenario we are selling content that already exists on the device; your Application simply makes a decision about whether to unlock it.

Unity IAP provides tools to help you hide secrets, validate and parse receipts on Google Play and Apple stores.

Obfuscating secrets

Receipt validation is performed using known encryption keys; your Application’s Google Play public key and/or Apple’s root certificate.

If an attacker can replace these secrets they will be able to defeat your receipt validation checks, so it is important to make difficult for an attacker to easily find and modify these keys.

Unity IAP provides a tool that can help you obfuscate your secret keys within your Application; the Obfuscator window accessible via Window > Unity IAP > IAP Receipt Validation Obfuscator.

The obfuscator window
The obfuscator window

This window will encode both Apple’s root certificate, which is bundled with Unity IAP, and your Google Play public key, into two different C# files that are added to your project, GooglePlayTangle and AppleTangle, for use in the next section.

Note that you do not have to provide a Google Play public key if you are only targeting Apple’s stores, or vice versa.

Validating receipts

The CrossPlatformValidator class can be used for validation across both Google Play and Apple stores.

You must supply this class with either your Google Play public key or Apple’s root certificate, or both if you wish to validate across both platforms.

The checks performed by the CrossPlatformValidator are as follows:

  1. Receipt authenticity is checked via signature validation
  2. Receipt Application bundle identifier is compared to your Application’s, with an InvalidBundleId exception thrown if they do not match

Note that the validator will only validate receipts generated on Google Play and Apple platforms. Receipts generated on any other platform, including the fakes generated in the Editor, will throw an IAPSecurityException.

If you attempt to validate a receipt for a platform for which you have not supplied the required secret, a MissingStoreSecretException will be thrown.

public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
    bool validPurchase = true; // Presume valid for platforms with no R.V.

    // Unity IAP's validation logic is only included on these platforms.
#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
    // Prepare the validator with the secrets we prepared in the Editor
    // obfuscation window.
    var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
        AppleTangle.Data(), Application.bundleIdentifier);

    try {
        // On Google Play, result will have a single product Id.
        // On Apple stores receipts contain multiple products.
        var result = validator.Validate(e.purchasedProduct.receipt);
        // For informational purposes, we list the receipt(s)
        Debug.Log("Receipt is valid. Contents:");
        foreach (IPurchaseReceipt productReceipt in result) {
            Debug.Log(productReceipt.productID);
            Debug.Log(productReceipt.purchaseDate);
            Debug.Log(productReceipt.transactionID);
        }
    } catch (IAPSecurityException) {
        Debug.Log("Invalid receipt, not unlocking content");
        validPurchase = false;
    }
#endif

    if (validPurchase) {
        // Unlock the appropriate content here.
    }

    return PurchaseProcessingResult.Complete;
}

It is important you check not just that the receipt is valid, but what information it contains.

A common attack vector is for hackers to supply receipts from other products or Applications; the receipts are genuine so will pass validation, so you should make decisions based on the product IDs parsed by the CrossPlatformValidator.

Store specific details

Different stores have different fields in their purchase receipts. To access store specific fields, IPurchaseReceipt can be downcast to two different subtypes, GooglePlayReceipt and AppleInAppPurchaseReceipt.

var result = validator.Validate(e.purchasedProduct.receipt);
Debug.Log("Receipt is valid. Contents:");
foreach (IPurchaseReceipt productReceipt in result) {
    Debug.Log(productReceipt.productID);
    Debug.Log(productReceipt.purchaseDate);
    Debug.Log(productReceipt.transactionID);

    GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
    if (null != google) {
        Debug.Log(google.purchaseState);
        Debug.Log(google.purchaseToken);
    }

    AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
    if (null != apple) {
        Debug.Log(apple.originalTransactionIdentifier);
        Debug.Log(apple.cancellationDate);
        Debug.Log(apple.quantity);
    }
}

Parsing raw Apple receipts

The AppleValidator class can be used to extract detailed information about an Apple receipt. Note that this class only works with iOS 7+ style App receipts, not Apple’s deprecated transaction receipts.

#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
// Get a reference to IAppleConfiguration during IAP initialization.
var appleConfig = builder.Configure<IAppleConfiguration>();
var receiptData = System.Convert.FromBase64String(appleConfig.appReceipt);
AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptData);

Debug.Log(receipt.bundleID);
Debug.Log(receipt.receiptCreationDate);
foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    Debug.Log(productReceipt.transactionIdentifier);
    Debug.Log(productReceipt.productIdentifier);
}
#endif

The AppleReceipt type models Apple’s ASN1 receipt format; see their documentation for an explanation of its fields.

Purchase Receipts
Store Extensions