/*
* Copyright 2015. Appsi Mobile
*
* 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.appsimobile.appsii.promo;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.appsimobile.appsii.BuildConfig;
import com.appsimobile.appsii.R;
import com.appsimobile.appsii.preference.ObfuscatedPreferences;
import com.google.android.vending.licensing.LicenseValidator;
import com.google.android.vending.licensing.util.Base64;
import com.google.android.vending.licensing.util.Base64DecoderException;
import java.io.UnsupportedEncodingException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Created by nick on 06/02/15.
*/
abstract class LicenseChecker {
public static final String RESPONSE_CODE = "0";
public static final String SIGNED_DATA = "1";
public static final String SIGNATURE = "2";
public static final boolean DEBUG = BuildConfig.TEST_PURCHASES;
final String mFeatureKey;
final String mFeature;
final Activity mContext;
final String mVersionCode;
final ObfuscatedPreferences mObfuscatedPreferences;
private final String mKey;
private final String mPackageName;
private final int mNonce;
PluginConnectionHelper mPluginConnectionHelper;
AsyncTask<Void, Void, Bundle> mTask;
LicenseChecker(Activity context, String packageName, String key, String feature,
ObfuscatedPreferences obfuscatedPreferences) {
mFeature = feature;
mObfuscatedPreferences = obfuscatedPreferences;
mFeatureKey = "unlocked" + mFeature;
// the key is already suffixed with 0
mKey = key;
mContext = context;
mPackageName = packageName;
mPluginConnectionHelper = new PluginConnectionHelper(
context,
mPackageName);
mNonce = mPluginConnectionHelper.mNonce;
mVersionCode = getVersionCode(context, packageName);
}
/**
* 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 (PackageManager.NameNotFoundException e) {
Log.e("Appsii", "Package not found. could not get version code.");
return "";
}
}
/**
* 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("RSA");
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("Appsii", "Could not decode from Base64.");
throw new IllegalArgumentException(e);
} catch (InvalidKeySpecException e) {
Log.e("Appsii", "Invalid key specification.");
throw new IllegalArgumentException(e);
}
}
// generate a hash
public static String sha256(String in) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(in.getBytes("UTF-8"));
return bytesToHexString(digest.digest());
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e1) {
return null;
}
}
// utility function
private static String bytesToHexString(byte[] bytes) {
// http://stackoverflow.com/questions/332079
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* Starts the check. Returns null if the plugin is not installed, the worker
* in case it is.
*/
public AsyncTask<?, ?, ?> checkAccess() {
try {
mContext.getPackageManager().getPackageInfo(mPackageName, 0);
} catch (PackageManager.NameNotFoundException e) {
Toast.makeText(mContext, R.string.plugin_not_installed, Toast.LENGTH_SHORT).show();
return null;
}
mTask = new AsyncTask<Void, Void, Bundle>() {
@Override
protected Bundle doInBackground(Void... params) {
if (DEBUG) Log.d("LicenseChecker", "in background, connecting to service");
final PluginConnectionHelper.AppsiPluginChecker checker =
mPluginConnectionHelper.connectToService();
if (DEBUG) Log.d("LicenseChecker", "got checker: " + checker);
if (checker == null) {
return null;
}
try {
if (DEBUG) Log.d("LicenseChecker", "await connection");
if (checker.waitForConnection()) {
if (DEBUG) Log.d("LicenseChecker", "connected, calling remote method");
return checker.checkLicense();
}
} catch (InterruptedException e) {
return null;
} catch (RemoteException e) {
if (DEBUG) Log.wtf("Promo", "error in rpc", e);
return null;
}
return null;
}
@Override
protected void onPostExecute(Bundle bundle) {
if (DEBUG) Log.d("LicenseChecker", "post exec. result: " + bundle);
checkResult(bundle);
}
};
// Do not block the main executor; run in a different pool
return mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Verifies that the Bundle contains a string "s" that is equal to
* salt + key + validation result (0 <- valid) + sha1 + mPackageName
* <p/>
* in which salt: is a random generated string (uuid)
* key is the google play license key
* sha1 is the locally calculated cache of the certificate key
* package-name is the target package-name
*/
void checkResult(Bundle bundle) {
try {
if (bundle == null) {
return;
}
int responseCode = bundle.getInt(RESPONSE_CODE);
String signedData = bundle.getString(SIGNED_DATA);
String signature = bundle.getString(SIGNATURE);
LicenseValidator validator = new LicenseValidator(mNonce, mPackageName, mVersionCode);
PublicKey publicKey = generatePublicKey(mKey);
int response = validator.verify(publicKey, responseCode, signedData, signature);
boolean verified = response ==
LicenseValidator.LICENSED_OLD_KEY || response == LicenseValidator.LICENSED;
if (mContext.getPackageManager().checkSignatures(
mPackageName, BuildConfig.APPLICATION_ID)
!= PackageManager.SIGNATURE_MATCH) {
throw new IllegalStateException("Unknown error");
}
String salt = mPluginConnectionHelper.mSalt;
if (salt == null) {
return;
}
String sha1;
try {
sha1 = PromoUnlockFragment.getCertificateFingerPrint(mContext, mPackageName);
} catch (PackageManager.NameNotFoundException e) {
return;
}
String keyWithResult = sha256(mKey + signedData + mNonce);
String key = salt + keyWithResult + sha1 + mPackageName;
if (DEBUG) Log.d("Appsii", "salt: " + salt);
if (DEBUG) Log.d("Appsii", "keyWithResult: " + keyWithResult);
if (DEBUG) Log.d("Appsii", "sha1: " + sha1);
if (DEBUG) Log.d("Appsii", "package: " + mPackageName);
String expectedSha1 = PromoUnlockFragment.sha1(key);
String bundleSha1 = bundle.getString("s");
if (bundleSha1 == null) {
return;
}
if (DEBUG) Log.d("Appsii", "expected: " + expectedSha1);
if (DEBUG) Log.d("Appsii", "received: " + bundleSha1);
if (TextUtils.equals(expectedSha1, bundleSha1) && verified) {
mObfuscatedPreferences.edit().putString(mFeature, mFeatureKey).apply();
}
} finally {
mPluginConnectionHelper.onDestroy();
mPluginConnectionHelper = null;
onCheckComplete(mPackageName);
}
}
protected abstract void onCheckComplete(String packageName);
}