diff options
Diffstat (limited to 'app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java')
-rw-r--r-- | app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java | 358 |
1 files changed, 358 insertions, 0 deletions
diff --git a/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java b/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java new file mode 100644 index 0000000..92986b8 --- /dev/null +++ b/app/src/main/java/moe/yuuta/gplicense/LicenseChecker.java @@ -0,0 +1,358 @@ +package moe.yuuta.gplicense; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +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 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.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + +import moe.yuuta.gplicense.ipc.ILicenseResultListener; +import moe.yuuta.gplicense.ipc.ILicensingService; +import moe.yuuta.gplicense.util.Base64; +import moe.yuuta.gplicense.util.Base64DecoderException; +import moe.yuuta.workmode.BuildConfig; + +/** + * 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 ServiceConnection { + 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 ILicensingService 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>(); + + /** + * @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.ILicensingService" + * <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 license 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 { + boolean bindResult = mContext + .bindService( + new Intent( + new String( + // Base64 encoded - + // com.android.vending.licensing.ILicensingService + // 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=="))), + this, // 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.checkLicense( + validator.getNonce(), validator.getPackageName(), + new ResultListener(validator)); + mChecksInProgress.add(validator); + } catch (RemoteException e) { + logger.w("RemoteException in checkLicense call.", e); + handleServiceConnectionError(validator); + } + } + } + + private synchronized void finishCheck(LicenseValidator validator) { + mChecksInProgress.remove(validator); + if (mChecksInProgress.isEmpty()) { + cleanupService(); + } + } + + private class ResultListener extends ILicenseResultListener.Stub { + 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. + 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); + } + } + + public synchronized void onServiceConnected(ComponentName name, IBinder service) { + mService = ILicensingService.Stub.asInterface(service); + runChecks(); + } + + 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(this); + } 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 context 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 |