/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.vending.licensing; 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.Looper; import android.os.RemoteException; import android.provider.Settings.Secure; import android.util.Log; import com.android.vending.licensing.ILicenseResultListener; import com.android.vending.licensing.ILicensingService; import com.google.android.vending.licensing.util.Base64; import com.google.android.vending.licensing.util.Base64DecoderException; 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.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Class that should be copied into each of the Appsi-Plugins. This class * can perform the call to the LicenseChecker. * This is a very much simplified version of the license-checker that runs * synchronously. */ public class LicenseChecker { static final String TAG = "LicenseChecker"; private static final String KEY_FACTORY_ALGORITHM = "RSA"; // Timeout value (in milliseconds) for calls to service. static final int TIMEOUT_MS = 8 * 1000; private static final SecureRandom RANDOM = new SecureRandom(); static final boolean DEBUG_LICENSE_ERROR = true; final Context mContext; private final Policy mPolicy; private final String mPackageName; private final String mVersionCode; final PublicKey mPublicKey; /** * A handler for running tasks on a background thread. We don't want license * processing to block the UI thread. */ final Handler mHandler; /** * @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, String packageName, Policy policy, String encodedPublicKey) { mContext = context; mPolicy = policy; mPublicKey = generatePublicKey(encodedPublicKey); mPackageName = packageName; 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) { Log.e(TAG, "Could not decode from Base64."); throw new IllegalArgumentException(e); } catch (InvalidKeySpecException e) { Log.e(TAG, "Invalid key specification."); throw new IllegalArgumentException(e); } } /** * Get version code for the application package name. * * @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) { Log.e(TAG, "Package not found. could not get version code."); return ""; } } public int checkLicense() throws InterruptedException, RemoteException { LicensingConnection service = connectSync(); LicenseValidator validator = new LicenseValidator(generateNonce(), mPackageName, mVersionCode); Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName()); ResultListener listener = new ResultListener(validator); service.mLicensingService.checkLicense( validator.getNonce(), validator.getPackageName(), listener); int result = listener.waitForResult(); try { mContext.unbindService(service.mServiceConnection); } catch (IllegalArgumentException e) { // Somehow we've already been unbound. This is a non-fatal // error. Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); } mHandler.getLooper().quit(); return result; } private LicensingConnection connectSync() throws InterruptedException { ensureNotOnMainThread(mContext); final BlockingQueue<ILicensingService> q = new LinkedBlockingQueue<>(1); ServiceConnection connection = new ServiceConnection() { volatile boolean mConnectedAtLeastOnce = false; @Override public void onServiceConnected(ComponentName name, IBinder service) { if (!mConnectedAtLeastOnce) { mConnectedAtLeastOnce = true; try { q.put(ILicensingService.Stub.asInterface(service)); } catch (InterruptedException e) { // will never happen, since the queue starts with one available slot } } } @Override public void onServiceDisconnected(ComponentName name) { } }; try { Intent intent = new Intent(new String(Base64.decode( "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))); boolean isBound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); if (!isBound) { throw new AssertionError("could not bind to LicensingService"); } return new LicensingConnection(connection, q.take()); } catch (Base64DecoderException e) { return null; } } /** * Generates a nonce (number used once). */ private int generateNonce() { return RANDOM.nextInt(); } private static void ensureNotOnMainThread(Context context) { Looper looper = Looper.myLooper(); if (looper != null && looper == context.getMainLooper()) { throw new IllegalStateException( "calling this from your main thread can lead to deadlock"); } } public boolean isLooperRunning() { return mHandler.sendEmptyMessage(444); } class LicensingConnection { final ServiceConnection mServiceConnection; final ILicensingService mLicensingService; public LicensingConnection(ServiceConnection connection, ILicensingService licensingService) { mServiceConnection = connection; mLicensingService = licensingService; } } private class ResultListener extends ILicenseResultListener.Stub { static final int ERROR_CONTACTING_SERVER = 0x101; static final int ERROR_INVALID_PACKAGE_NAME = 0x102; static final int ERROR_NON_MATCHING_UID = 0x103; final CountDownLatch mCountDownLatch = new CountDownLatch(1); final AtomicBoolean mTimedOut = new AtomicBoolean(); final AtomicInteger mResult = new AtomicInteger(Policy.NOT_LICENSED); final LicenseValidator mValidator; private final Runnable mOnTimeout; public ResultListener(LicenseValidator validator) { mValidator = validator; mOnTimeout = new Runnable() { public void run() { Log.i(TAG, "Check timed out."); mTimedOut.set(true); mCountDownLatch.countDown(); } }; startTimeout(); } private void startTimeout() { Log.i(TAG, "Start monitoring timeout."); mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); } // 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(new Runnable() { public void run() { try { Log.i(TAG, "Received response."); // Make sure it hasn't already timed out. if (!mTimedOut.get()) { clearTimeout(); mValidator.verify(mPublicKey, responseCode, signedData, signature); } 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) { String android_id = Secure.getString(mContext.getContentResolver(), Secure.ANDROID_ID); Date date = new Date(); Log.d(TAG, "Server Failure: " + stringError); Log.d(TAG, "Android ID: " + android_id); Log.d(TAG, "Time: " + date.toGMTString()); } } } finally { mCountDownLatch.countDown(); } } }); } void clearTimeout() { Log.i(TAG, "Clearing timeout."); mHandler.removeCallbacks(mOnTimeout); } public int waitForResult() throws InterruptedException { mCountDownLatch.await(); return mResult.get(); } } }