aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/moe/yuuta/gplicense/LicenseValidator.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/moe/yuuta/gplicense/LicenseValidator.java')
-rw-r--r--app/src/main/java/moe/yuuta/gplicense/LicenseValidator.java219
1 files changed, 219 insertions, 0 deletions
diff --git a/app/src/main/java/moe/yuuta/gplicense/LicenseValidator.java b/app/src/main/java/moe/yuuta/gplicense/LicenseValidator.java
new file mode 100644
index 0000000..142f350
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/gplicense/LicenseValidator.java
@@ -0,0 +1,219 @@
+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 license check.");
+ 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);
+ }
+} \ No newline at end of file