Verify Google Play App Purchase on Your Server
singpolyma@singpolyma.net
We are preparing for the first-ever Google Play Store launch of Cheogram Android as part of JMP coming out of beta later this year. One of the things we wanted to “just work” for Google Play users is to be able to pay for the app and get their first month of JMP “bundled” into that purchase price, to smooth the common onboarding experience. So how do the JMP servers know that the app communicating with them is running a version of the app bought from Google Play as opposed to our builds, F-Droid’s builds, or someone’s own builds? And also ensure that this person hasn’t already got a bundled month before? The documentation available on how to do this is surprisingly sparse, so let’s do this together.
Client Side
Google publishes an official Licensing Verification Library for communicating with Google Play from inside an Android app to determine if this install of the app can be associated with a Google Play purchase. Most existing documentation focuses on using this library, however it does not expose anything in the callbacks other than “yes license verified” or “no, not verified”. This can allow an app to check if it is a purchased copy itself, but is not so useful for communicating that proof onward to a server. The library also contains some exciting snippets like:
// Base64 encoded -
// com.android.vending.licensing.ILicensingService
// Consider encoding this in another way in your
// code to imp rove security
Base64.decode(
"Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
Which implies that they expect developers to fork this code to use it. Digging in to the code we find in LicenseValidator.java:
public void verify(PublicKey publicKey, int responseCode, String signedData, String signature)
Which looks like exactly what we need: the actual signed assertion from Google Play and the signature! So we just need a small patch to pass those along to the callback as well as the response code currently being passed. Then we can use the excellent jitpack to include the forked library in our app:
implementation 'com.github.singpolyma:play-licensing:1c637ea03c'
Then we write a small class in our app code to actually use it:
import android.content.Context;
import com.google.android.vending.licensing.*;
import java.util.function.BiConsumer;
public class CheogramLicenseChecker implements LicenseCheckerCallback {
private final LicenseChecker mChecker;
private final BiConsumer mCallback;
public CheogramLicenseChecker(Context context, BiConsumer<String, String> callback) {
mChecker = new LicenseChecker(
context,
new StrictPolicy(), // Want to get a signed item every time
context.getResources().getString(R.string.licensePublicKey)
);
mCallback = callback;
}
public void checkLicense() {
mChecker.checkAccess(this);
}
@Override
public void dontAllow(int reason) {
mCallback.accept(null, null);
}
@Override
public void applicationError(int errorCode) {
mCallback.accept(null, null);
}
@Override
public void allow(int reason, ResponseData data, String signedData, String signature) {
mCallback.accept(signedData, signature);
}
}
Here we use the StrictPolicy from the License Verification Library because we want to get a fresh signed data every time, and if the device is offline the whole question is moot because we won’t be able to contact the server anyway.
This code assumes you put the Base64 encoded licensing public key from “Monetisation Setup” in Play Console into a resource R.string.licensePublicKey
.
Then we need to communicate this to the server, which you can do whatever way makes sense for your protocol; with XMPP we can easily add custom elements to our existing requests so:
new com.cheogram.android.CheogramLicenseChecker(context, (signedData, signature) -> {
if (signedData != null && signature != null) {
c.addChild("license", "https://ns.cheogram.com/google-play").setContent(signedData);
c.addChild("licenseSignature", "https://ns.cheogram.com/google-play").setContent(signature);
}
xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
session.updateWithResponse(iq);
});
}).checkLicense();
Server Side
When trying to verify this on the server side we quickly run into some new issues. What format is this public key in? It just says “public key” and is Base64 but that’s about it. What signature algorithm is used for the signed data? What is the format of the data itself? Back to the library code!
private static final String KEY_FACTORY_ALGORITHM = "RSA";
…
byte[] decodedKey = Base64.decode(encodedPublicKey);
…
new X509EncodedKeySpec(decodedKey)
So we can see it is an X509 related encoded, and indeed turns out to be Base64 encoded DER. So we can run this:
echo "BASE64_STRING" | base64 -d | openssl rsa -pubin -inform der -in - -text
to get the raw properties we might need for any library (key size, modulus, and exponent). Of course, if your library supports parsing DER directly you can also use that.
import java.security.Signature;
…
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
…
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
Combined with the java documentation we can thus say that the signature algoritm is PKCS#1 padded RSA with SHA1.
And finally:
String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
data.responseCode = Integer.parseInt(fields[0]);
data.nonce = Integer.parseInt(fields[1]);
data.packageName = fields[2];
data.versionCode = fields[3];
// Application-specific user identifier.
data.userId = fields[4];
data.timestamp = Long.parseLong(fields[5]);
The format of the data, pipe-seperated text. The main field of interest for us is userId which is (as it says in a comment) “a user identifier unique to the <application, user> pair”. So in our server code:
import Control.Error (atZ)
import qualified Data.ByteString.Base64 as Base64
import qualified Data.Text as T
import Crypto.Hash.Algorithms (SHA1(SHA1))
import qualified Crypto.PubKey.RSA as RSA
import qualified Crypto.PubKey.RSA.PKCS15 as RSA
import qualified Data.XML.Types as XML
googlePlayUserId
| googlePlayVerified = (T.split (=='|') googlePlayLicense) `atZ` 4
| otherwise = Nothing
googlePlayVerified = fromMaybe False $ fmap (\pubKey ->
RSA.verify (Just SHA1) pubKey (encodeUtf8 googlePlayLicense)
(Base64.decodeLenient $ encodeUtf8 googlePlaySig)
) googlePlayPublicKey
googlePlayLicense = mconcat $ XML.elementText
=<< XML.isNamed (s"{https://ns.cheogram.com/google-play}license")
=<< XML.elementChildren payload
googlePlaySig = mconcat $ XML.elementText
=<< XML.isNamed (s"{https://ns.cheogram.com/google-play}licenseSignature")
=<< XML.elementChildren payload
We can then use the verified and extracted googlePlayUserId
value to check if this user has got a bundled month before and, if not, to provide them with one during signup.