aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java')
-rw-r--r--app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java356
1 files changed, 0 insertions, 356 deletions
diff --git a/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java b/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java
deleted file mode 100644
index e909aec..0000000
--- a/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java
+++ /dev/null
@@ -1,356 +0,0 @@
-package moe.yuuta.gplicense;
-
-import android.annotation.SuppressLint;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.provider.Settings.Secure;
-import com.elvishew.xlog.Logger;
-import com.elvishew.xlog.XLog;
-import moe.yuuta.ext.*;
-import moe.yuuta.gplicense.util.Base64;
-import moe.yuuta.gplicense.util.Base64DecoderException;
-import moe.yuuta.workmode.BuildConfig;
-
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.*;
-
-/**
- * Client library for Google Play license verifications.
- * <p>
- * The LicenseChecker is configured via a {@link Policy} which contains the logic to determine
- * whether a user should have access to the application. For example, the Policy can define a
- * threshold for allowable number of server or client failures before the library reports the user
- * as not having access.
- * <p>
- * Must also provide the Base64-encoded RSA public key associated with your developer account. The
- * public key is obtainable from the publisher site.
- */
-public class LicenseChecker implements ConnCallback {
- private static final Logger logger = XLog.tag("LCK").build();
-
- private static final String KEY_FACTORY_ALGORITHM = "RSA";
-
- // Timeout value (in milliseconds) for calls to service.
- private static final int TIMEOUT_MS = 10 * 1000;
-
- private static final SecureRandom RANDOM = new SecureRandom();
- private static final boolean DEBUG_LICENSE_ERROR = BuildConfig.DEBUG;
-
- private IService mService;
-
- private PublicKey mPublicKey;
- private final Context mContext;
- private final Policy mPolicy;
- /**
- * A handler for running tasks on a background thread. We don't want license processing to block
- * the UI thread.
- */
- private Handler mHandler;
- private final String mPackageName;
- private final String mVersionCode;
- private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
- private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
-
- private LicServiceConn mConn;
-
- /**
- * @param context a Context
- * @param policy implementation of Policy
- * @param encodedPublicKey Base64-encoded RSA public key
- * @throws IllegalArgumentException if encodedPublicKey is invalid
- */
- public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
- mContext = context;
- mPolicy = policy;
- mPublicKey = generatePublicKey(encodedPublicKey);
- mPackageName = mContext.getPackageName();
- mVersionCode = getVersionCode(context, mPackageName);
- HandlerThread handlerThread = new HandlerThread("background thread");
- handlerThread.start();
- mHandler = new Handler(handlerThread.getLooper());
- }
-
- /**
- * 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
- */
- private static PublicKey generatePublicKey(String encodedPublicKey) {
- try {
- byte[] decodedKey = Base64.decode(encodedPublicKey);
- KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
-
- return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
- } catch (NoSuchAlgorithmException e) {
- // This won't happen in an Android-compatible environment.
- throw new RuntimeException(e);
- } catch (Base64DecoderException e) {
- logger.e("Could not decode from Base64.");
- throw new IllegalArgumentException(e);
- } catch (InvalidKeySpecException e) {
- logger.e("Invalid key specification.");
- throw new IllegalArgumentException(e);
- }
- }
-
- /**
- * Checks if the user should have access to the app. Binds the service if necessary.
- * <p>
- * NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we
- * recommend obfuscating the string that is passed into bindService using another method of your
- * own devising.
- * <p>
- * source string: "com.android.vending.licensing.IService"
- * <p>
- *
- * @param callback
- */
- public synchronized void checkAccess(LicenseCheckerCallback callback) {
- // If we have a valid recent LICENSED response, we can skip asking
- // Market.
- if (mPolicy.allowAccess()) {
- logger.i("Using cached response");
- callback.allow(Policy.LICENSED);
- } else {
- LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
- callback, generateNonce(), mPackageName, mVersionCode);
-
- if (mService == null) {
- logger.i("Binding to service.");
- try {
- mConn = new LicServiceConn(this);
- boolean bindResult = mContext
- .bindService(
- new Intent(
- new String(
- // Base64 encoded -
- // com.android.vending.licensing.IService
- // Consider encoding this in another way in your
- // code to improve security
- Base64.decode(
- "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
- // As of Android 5.0, implicit
- // Service Intents are no longer
- // allowed because it's not
- // possible for the user to
- // participate in disambiguating
- // them. This does mean we break
- // compatibility with Android
- // Cupcake devices with this
- // release, since setPackage was
- // added in Donut.
- .setPackage(
- new String(
- // Base64
- // encoded -
- // com.android.vending
- Base64.decode(
- "Y29tLmFuZHJvaWQudmVuZGluZw=="))),
- mConn, // ServiceConnection.
- Context.BIND_AUTO_CREATE);
- if (bindResult) {
- mPendingChecks.offer(validator);
- } else {
- logger.e("Could not bind to service.");
- handleServiceConnectionError(validator);
- }
- } catch (SecurityException e) {
- callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
- } catch (Base64DecoderException e) {
- e.printStackTrace();
- }
- } else {
- mPendingChecks.offer(validator);
- runChecks();
- }
- }
- }
-
- private void runChecks() {
- LicenseValidator validator;
- while ((validator = mPendingChecks.poll()) != null) {
- try {
- logger.i("Executing on service for " + validator.getPackageName());
- mService.exec(
- validator.getNonce(), validator.getPackageName(),
- new IPCResultListener(new ResultListener(validator)));
- mChecksInProgress.add(validator);
- } catch (RemoteException e) {
- logger.w("RemoteException in exec call.", e);
- handleServiceConnectionError(validator);
- }
- }
- }
-
- private synchronized void finishCheck(LicenseValidator validator) {
- mChecksInProgress.remove(validator);
- if (mChecksInProgress.isEmpty()) {
- cleanupService();
- }
- }
-
- private class ResultListener implements ResultCallback {
- private final LicenseValidator mValidator;
- private Runnable mOnTimeout;
-
- public ResultListener(LicenseValidator validator) {
- mValidator = validator;
- mOnTimeout = () -> {
- logger.i("Check timed out.");
- handleServiceConnectionError(mValidator);
- finishCheck(mValidator);
- };
- startTimeout();
- }
-
- private static final int ERROR_CONTACTING_SERVER = 0x101;
- private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
- private static final int ERROR_NON_MATCHING_UID = 0x103;
-
- // Runs in IPC thread pool. Post it to the Handler, so we can guarantee
- // either this or the timeout runs.
- @Override
- public void verifyLicense(final int responseCode, final String signedData,
- final String signature) {
- mHandler.post(() -> {
- logger.i("Received response. (code: " + responseCode + ")");
- // Make sure it hasn't already timed out.
- if (mChecksInProgress.contains(mValidator)) {
- clearTimeout();
- mValidator.verify(mPublicKey, responseCode, signedData, signature);
- finishCheck(mValidator);
- }
- if (DEBUG_LICENSE_ERROR) {
- boolean logResponse;
- String stringError = null;
- switch (responseCode) {
- case ERROR_CONTACTING_SERVER:
- logResponse = true;
- stringError = "ERROR_CONTACTING_SERVER";
- break;
- case ERROR_INVALID_PACKAGE_NAME:
- logResponse = true;
- stringError = "ERROR_INVALID_PACKAGE_NAME";
- break;
- case ERROR_NON_MATCHING_UID:
- logResponse = true;
- stringError = "ERROR_NON_MATCHING_UID";
- break;
- default:
- logResponse = false;
- }
-
- if (logResponse) {
- @SuppressLint("HardwareIds") String android_id = Secure.getString(mContext.getContentResolver(),
- Secure.ANDROID_ID);
- Date date = new Date();
- logger.d("Server Failure: " + stringError);
- logger.d("Android ID: " + android_id);
- logger.d("Time: " + date.toGMTString());
- }
- }
-
- });
- }
-
- private void startTimeout() {
- logger.i("Start monitoring timeout.");
- mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
- }
-
- private void clearTimeout() {
- logger.i("Clearing timeout.");
- mHandler.removeCallbacks(mOnTimeout);
- }
- }
-
- @Override
- public synchronized void onServiceConnected(ComponentName name, IBinder service) {
- mService = IService.Stub.asInterface(service);
- runChecks();
- }
-
- @Override
- public synchronized void onServiceDisconnected(ComponentName name) {
- // Called when the connection with the service has been
- // unexpectedly disconnected. That is, Market crashed.
- // If there are any checks in progress, the timeouts will handle them.
- logger.w("Service unexpectedly disconnected.");
- mService = null;
- }
-
- /**
- * Generates policy response for service connection errors, as a result of disconnections or
- * timeouts.
- */
- private synchronized void handleServiceConnectionError(LicenseValidator validator) {
- mPolicy.processServerResponse(Policy.RETRY, null);
-
- if (mPolicy.allowAccess()) {
- validator.getCallback().allow(Policy.RETRY);
- } else {
- validator.getCallback().dontAllow(Policy.RETRY);
- }
- }
-
- /** Unbinds service if necessary and removes reference to it. */
- private void cleanupService() {
- if (mService != null) {
- try {
- mContext.unbindService(mConn);
- } catch (IllegalArgumentException e) {
- // Somehow we've already been unbound. This is a non-fatal
- // error.
- logger.e("Unable to unbind from licensing service (already unbound)");
- }
- mService = null;
- }
- }
-
- /**
- * Inform the library that the hostContext is about to be destroyed, so that any open connections
- * can be cleaned up.
- * <p>
- * Failure to call this method can result in a crash under certain circumstances, such as during
- * screen rotation if an Activity requests the license check or when the user exits the
- * application.
- */
- public synchronized void onDestroy() {
- cleanupService();
- mHandler.getLooper().quit();
- }
-
- /** Generates a nonce (number used once). */
- private int generateNonce() {
- return RANDOM.nextInt();
- }
-
- /**
- * Get version code for the application package name.
- *
- * @param context
- * @param packageName application package name
- * @return the version code or empty string if package not found
- */
- private static String getVersionCode(Context context, String packageName) {
- try {
- return String.valueOf(
- context.getPackageManager().getPackageInfo(packageName, 0).versionCode);
- } catch (NameNotFoundException e) {
- logger.e("Package not found. could not get version code.");
- return "";
- }
- }
-} \ No newline at end of file