package moe.yuuta.gplicense; import android.text.TextUtils; import com.elvishew.xlog.Logger; import com.elvishew.xlog.XLog; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import moe.yuuta.gplicense.util.Base64; import moe.yuuta.gplicense.util.Base64DecoderException; /** * Contains data related to a licensing request and methods to verify * and process the response. */ class LicenseValidator { private final Logger logger = XLog.tag("LVter").build(); // Server response codes. private static final int LICENSED = 0x0; private static final int NOT_LICENSED = 0x1; private static final int LICENSED_OLD_KEY = 0x2; private static final int ERROR_NOT_MARKET_MANAGED = 0x3; private static final int ERROR_SERVER_FAILURE = 0x4; private static final int ERROR_OVER_QUOTA = 0x5; 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; private final Policy mPolicy; private final LicenseCheckerCallback mCallback; private final int mNonce; private final String mPackageName; private final String mVersionCode; private final DeviceLimiter mDeviceLimiter; LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback, int nonce, String packageName, String versionCode) { mPolicy = policy; mDeviceLimiter = deviceLimiter; mCallback = callback; mNonce = nonce; mPackageName = packageName; mVersionCode = versionCode; } public LicenseCheckerCallback getCallback() { return mCallback; } public int getNonce() { return mNonce; } public String getPackageName() { return mPackageName; } private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; /** * Verifies the response from server and calls appropriate callback method. * * @param publicKey public key associated with the developer account * @param responseCode server response code * @param signedData signed data from server * @param signature server signature */ public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) { String userId = null; // Skip signature check for unsuccessful requests ResponseData data = null; if (responseCode == LICENSED || responseCode == NOT_LICENSED || responseCode == LICENSED_OLD_KEY) { // Verify signature. try { if (TextUtils.isEmpty(signedData)) { logger.e("Signature verification failed: signedData is empty. " + "(Device not signed-in to any Google accounts?)"); handleInvalidResponse(); return; } Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(publicKey); sig.update(signedData.getBytes()); if (!sig.verify(Base64.decode(signature))) { logger.e("Signature verification failed."); handleInvalidResponse(); return; } } catch (NoSuchAlgorithmException e) { // This can't happen on an Android compatible device. throw new RuntimeException(e); } catch (InvalidKeyException e) { handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); return; } catch (SignatureException e) { throw new RuntimeException(e); } catch (Base64DecoderException e) { logger.e("Could not Base64-decode signature."); handleInvalidResponse(); return; } // Parse and validate response. try { data = ResponseData.parse(signedData); } catch (IllegalArgumentException e) { logger.e("Could not parse response."); handleInvalidResponse(); return; } if (data.responseCode != responseCode) { logger.e("Response codes don't match."); handleInvalidResponse(); return; } if (data.nonce != mNonce) { logger.e("Nonce doesn't match."); handleInvalidResponse(); return; } if (!data.packageName.equals(mPackageName)) { logger.e("Package name doesn't match."); handleInvalidResponse(); return; } if (!data.versionCode.equals(mVersionCode)) { logger.e("Version codes don't match."); handleInvalidResponse(); return; } // Application-specific user identifier. userId = data.userId; if (TextUtils.isEmpty(userId)) { logger.e("User identifier is empty."); handleInvalidResponse(); return; } } logger.d("Final code: " + responseCode); switch (responseCode) { case LICENSED: case LICENSED_OLD_KEY: int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); handleResponse(limiterResponse, data); break; case NOT_LICENSED: handleResponse(Policy.NOT_LICENSED, data); break; case ERROR_CONTACTING_SERVER: logger.w("Error contacting licensing server."); handleResponse(Policy.RETRY, data); break; case ERROR_SERVER_FAILURE: logger.w("An error has occurred on the licensing server."); handleResponse(Policy.RETRY, data); break; case ERROR_OVER_QUOTA: logger.w("Licensing server is refusing to talk to this device, over quota."); handleResponse(Policy.RETRY, data); break; case ERROR_INVALID_PACKAGE_NAME: handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME); break; case ERROR_NON_MATCHING_UID: handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID); break; case ERROR_NOT_MARKET_MANAGED: handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED); break; default: logger.e("Unknown response code for checking."); handleInvalidResponse(); } } /** * Confers with policy and calls appropriate callback method. * * @param response * @param rawData */ private void handleResponse(int response, ResponseData rawData) { logger.d("handle: " + response); // Update policy data and increment retry counter (if needed) mPolicy.processServerResponse(response, rawData); // Given everything we know, including cached data, ask the policy if we should grant // access. if (mPolicy.allowAccess()) { mCallback.allow(response); } else { mCallback.dontAllow(response); } } private void handleApplicationError(int code) { mCallback.applicationError(code); } private void handleInvalidResponse() { mCallback.dontAllow(Policy.NOT_LICENSED); } }