Working around API-purchase-logic-flaws for consumables in Google Play's Billing API v3 (Relevant to everyone using consumables with API v3)

て烟熏妆下的殇ゞ 提交于 2019-11-28 09:24:17

Here's the simplest way to fix all this, that I have come up with so far. It's not the most elegant approach, but at least it should work:

  1. Generate a globally unique purchase ID and store it locally on the device.
  2. Launch a purchase flow with getBuyIntent with the purchase ID as the developer payload.
  3. Get a response Bundle from Google Play indicating if the purchase completed successfully.
  4. If purchase was successful, provision the product and remember the purchase ID as completed (this must be done atomically).
  5. If the provisioning was successful, consume the purchase by making a consumePurchase call
    (I do this in a "fire-and-forget" manner).

Every time the app is launched, go through the following:

  1. Send a getPurchases request to query the owned in-app products for the user.
  2. If any consumable products are found, check if the purchase ID in the developer payload is stored on the device. If not, ignore the product.
  3. For products with a "local" purchase ID, check if the purchase ID is included in the completed-list. If not, continue at step 4 above, otherwise continue at step 5 above.

Here's how things can go wrong on a single device and what happens then:

  • If the purchase never starts or doesn't complete, the user doesn't get charged and the app goes back to the pre-purchase-state and the user can try again. The unused purchase ID still is in the "local"-list, but that should only be a fairly minor "memory-leak" that can be fixed with some expiration-logic.
  • If the purchase completes, but the app dies before step 4, when it gets restarted, it finds the pending purchase (the product is still reported as owned) and can continue with step 4.
  • If the app dies after step 4 but before the product is consumed, the app finds the pending purchase on restart, but knows to ignore it as the purchase ID is in the completed-list. The app simply continues with step 5.

In the multiple-device-case, any other device will simply ignore any non-local pending purchases (consumables reported as owned) as the purchase ID is not in that device's local list.

The one issue is that a pending purchase will prevent other devices from being able to start a parallel purchase for the same product. So, if a user has an incomplete transaction stuck somewhere between step 2 and 5 (i.e. after purchase completion, but before consumption completion) on his phone, he won't be able to do any more purchases of the same product on his tablet until the app completes step 5, i.e. consumes the product, on the phone.

This issue can be resolved very easily (but not elegantly) by adding multiple copies (5 maybe?) of each consumable SKU to Google Play and changing step 2 in the first list to:

  1. Launch a purchase flow for the next available SKU in the set with getBuyIntent with the purchase ID as the developer payload.

A note on hackability (in order of increasing difficulty for the hacker):

  1. Completing fake purchases via Freedom APK or similar:
    These apps basically impersonate the Google Play Store to complete the purchase. To detect them, one needs to verify the signature included in the purchase receipt and reject purchases that fail the check, which most apps don't do (right). Problem solved in most cases (see point 4).
  2. Increasing in-app account balance of consumable via Game Killer or similar:
    These apps will try to figure out where in memory (or local storage) your app stores the current number of coins or other consumable products to modify the number directly. To make this harder (i.e. impossible for the average user), one needs to come up with a way to store the account balance not as a "plain-text" integer, but in some encrypted way or along with some checksums. Problem solved in most cases (see point 4).
  3. Killing the app at the right time and messing with its local storage:
    If someone purchases a consumable product on their phone and manages to kill the app after the product has been provisioned but before it has been consumed (likely very difficult to force), they could then modify the local storage on their tablet to add the purchase ID to the local list to have the product awarded once on each device. Or, they could corrupt the list of completed purchase IDs on the phone and restart the app to get the award twice. If they again manage to kill the app after provisioning but before consumption of the product (easy now by simply setting the phone to airplane mode and deleting the Google Play Store Cache), they can keep stealing more and more product in this way. Again, obfuscating or checksumming the storage can make this much harder.
  4. Decompiling and developing a patch for the app:
    This approach, of course, allows the hacker to pretty much do anything they want with your app (including breaking any countermeasures taken to alleviate points 1 and 2) and it will be extremely hard to prevent entirely. But it can be made harder for the hacker by using code obfuscation (ProGuard) and overly complex logic for the critical purchase-management code (might lead to buggy code, though, so this is not necessarily the best idea). Also, the code can be written in a way that its logic can be modified without affecting its function to allow for regular deployment of alternate versions that break any available patches.

Overall, signature verification for the purchases and some relatively simple but non-obvious checksumming or signing of the relevant data (in memory and in the local storage) should be sufficient to force a hacker to decompile (or otherwise reverse-engineer) the app in order to steal product. Unless the app gets hugely popular this should be a sufficient deterrent. Flexible logic in the code combined with somewhat frequent updates that break any developed patches can keep the app a moving target for hackers.

Keep in mind that I might be forgetting some other hacks. Please comment if you know of one.

Conclusion:

Overall, this is not the cleanest solution as one needs to maintain multiple parallel SKUs for each consumable product, but so far I haven't come up with a better one that actually fixes the issues.

So, please do share any other ideas you might have. +1`s guaranteed for any good pointers. :)

First of all I want to say I agree with everything you wrote. The problem exists and I would try to solve it similarly to how you did it. I would really suggest to find someone from Google Play relation team and make them aware of it.

Now back to your solution. This is probably the best standalone solution involving no server I could think about. It's simple but fairly good. One place where it can be misused would be when attackers would fake journal file and "buy" whatever they want, because getPurchases won't return anything from a manipulated journal file.

Otherwise, what else I would try to do is to reduce a probability the app gets killed by the system. For that you might extract purchasing and consumption logic into a smaller foreground service running in a separate process. This will increase probability the service finishes its work, even when Android will kill the bigger game application. More complex, but also a more reliable solution would be to implement journal on the server and share it between devices. With this solution you can always check whether someone is cheating with the purchases and even solve the issue when multiple devices are involved.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!