3

When using in-app billing, you should verify the purchase data, by checking if the INAPP_PURCHASE_DATA was signed with the INAPP_DATA_SIGNATURE by using the base64 encoded public key from Google Play store.
See the explanation of INAPP_PURCHASE_DATA and INAPP_DATA_SIGNATURE here. There is a Security class you can use to verify the purchase:

public class Security {
    private static final String TAG = "IABUtil/Security";

    private static final String KEY_FACTORY_ALGORITHM = "RSA";
    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";

    /**
     * Verifies that the data was signed with the given signature, and returns
     * the verified purchase. The data is in JSON format and signed
     * with a private key. The data also contains the {@link PurchaseState}
     * and product ID of the purchase.
     * @param base64PublicKey the base64-encoded public key to use for verifying.
     * @param signedData the signed JSON string (signed, not encrypted)
     * @param signature the signature for the data, signed with the private key
     */
    public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
                TextUtils.isEmpty(signature)) {
            Log.e(TAG, "Purchase verification failed: missing data.");
            return false;
        }

        PublicKey key = Security.generatePublicKey(base64PublicKey);
        return Security.verify(key, signedData, signature);
    }

    /**
     * Generates a PublicKey instance from a string containing the
     * Base64-encoded public key.
     *
     * @param encodedPublicKey Base64-encoded public key
     * @throws IllegalArgumentException if encodedPublicKey is invalid
     */
    public static PublicKey generatePublicKey(String encodedPublicKey) {
        try {
            byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeySpecException e) {
            Log.e(TAG, "Invalid key specification.");
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Verifies that the signature from the server matches the computed
     * signature on the data.  Returns true if the data is correctly signed.
     *
     * @param publicKey public key associated with the developer account
     * @param signedData signed data from server
     * @param signature server signature
     * @return true if the data and signature match
     */
    public static boolean verify(PublicKey publicKey, String signedData, String signature) {
        byte[] signatureBytes;
        try {
            signatureBytes = Base64.decode(signature, Base64.DEFAULT);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Base64 decoding failed.");
            return false;
        }
        try {
            Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
            sig.initVerify(publicKey);
            sig.update(signedData.getBytes());
            if (!sig.verify(signatureBytes)) {
                Log.e(TAG, "Signature verification failed.");
                return false;
            }
            return true;
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "NoSuchAlgorithmException.");
        } catch (InvalidKeyException e) {
            Log.e(TAG, "Invalid key specification.");
        } catch (SignatureException e) {
            Log.e(TAG, "Signature exception.");
        }
        return false;
    }
}

You have to invoke the verifyPurchase and pass the purchase data, the given signature and the base64PublicKey public key from Google Play. There is a wrapper implementation I can use for the purchase flow in my app.
If you look into the IabHelper implementation, they pass the public key for the verification in constructor. The documentation of the ctor says:

 * @param base64PublicKey Your application's public key, encoded in base64.
 *     This is used for verification of purchase signatures. You can find your app's base64-encoded
 *     public key in your application's page on Google Play Developer Console. Note that this
 *     is NOT your "developer public key".
 */

I guess they mean the Base64-encoded RSA public key in Licensing & in-app billing section in Google Play:

pixelized key

Maybe I don't know enough about cryptography but how is this possible that I use a public key from Google Play to check an encryption which is supposedly made with my "developers private key" (see explanation in first link). Do they mean my "private key I used to sign the app"? I don't think so, because they cannot know my (local) private key (I use to sign my app) and what does it have to do with this public key from Google Play, so what do they mean with "developer's private key".
So my questions are:

  • Did I understand it right that the public key is the key from Licensing & in-app billing?

  • Do I also need to add licensing to my app to get this verification working or should this work "out of the box", so can I omit this step?

  • What is the "developer's private key" Google is using to sign the purchase data and where do I see it? (I need to run some Unit tests on my server to check my implementation and I want to encrypt the INAPP_PURCHASE_DATA to get the INAPP_DATA_SIGNATURE as well to be able to get a valid security check if I verify it with the given public key.

[UPDATE]. Obviously, the private key is hidden:

The Google Play Console exposes the public key for licensing to any developer signed in to the Play Console, but it keeps the private key hidden from all users in a secure location.

See: https://developer.android.com/google/play/licensing/adding-licensing.html

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Bevor
  • 8,396
  • 15
  • 77
  • 141

1 Answers1

2

There are two types of signature asymmetric & symmetric :

Asymm uses a pair of keys, the private and the public, the keys have a mathematical relationship between them, one chunk of data signed with the private key can be verified with the public one. The private key is never published, but the public is.

Then Google created a pair of keys for your in-app billing ... but you only need to know the public to verify. No body will generate a valid signature without the private key.

Instead Symm uses the same key in both sides, that poses the problem to share the key with the risk to be sniffed, but it has the advantage to be faster than asymm.

UPDATE

Do I also need to add licensing to my app to get this verification working or should this work "out of the box", so can I omit this step?

Depends, if you want to know if the app has been installed from the official Google Play Store then you need verify licensing, that applies better if your app is a paid app, instead if your app is free but it has in-app products the important thing is to know if they purchased legally the item.

For me it is more important to verify purchases in a external server, there you have a nice example https://stackoverflow.com/a/48645216/7690376

from56
  • 3,976
  • 2
  • 13
  • 23
  • Thanks, but I basically I know how asymmetric encryption works. That's why I was confused about the public key because I didn't know where the private key was. This question was already answered by myself. Now I need an answer to my question, if I need licensing to get all this working. – Bevor Feb 18 '18 at 18:01
  • Sorry but if you are confused with a public and a private key then you don't know what asymmetric signature is ... I updated my answer – from56 Feb 18 '18 at 18:19
  • Ok, so if I understand it right, it is basically not needed for the purchase verification to use licensing. I will upvote for now and accept when I finished my implementation according to this information. If it's not needed then I don't care about it, because people cannot fake my purchase since everything is handled on my server and my server does the Google Developer API requests to check the status. By the way, your verification described in the link is in my opinion unsecure. What does prevent me as hacker to just mock this request and always return "true"? – Bevor Feb 19 '18 at 08:00
  • Thanks for posting that Security class in your question. With a few customization, I was able to set it up for verification in my app for InApp Billing. Thanks again. @Bevor – i_o Jul 08 '18 at 08:40
  • @i_o Glad to were able to help you. (btw: This was not my final version too, although I only had to make very few changes. Instead of `Base64.decode(...)` I had to use `Base64.decodeBase64(...)`). – Bevor Jul 09 '18 at 11:47
  • @i_o the answer with the code is mine not from bevor, ,,, he only criticized it, although he seems to have used it later – from56 Jul 09 '18 at 17:31
  • @LluisFelisart Hey thanks man... its a very good piece of code.. Simple and to the point. – i_o Jul 10 '18 at 02:38
  • @i_o The Security class is posted by my initial question and it was linked to the site below (Lluis, why do you claim, that anything is posted by you although there is no Java code part of your answer. So what are you actually talking about?). The code is a public example from here (as linked in my question, not in Lluis' answer): https://github.com/googlesamples/android-play-billing/blob/master/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util/Security.java How should I know "Lluis'" code in my QUESTION? Is this some kind of "effect comes before cause" or what? – Bevor Aug 04 '18 at 07:13