diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt index 0fb29408..4180119a 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt @@ -4,6 +4,9 @@ import android.app.Activity import android.content.Context import com.android.billingclient.api.* import net.pokeranalytics.android.ui.activity.IAPProducts +import timber.log.Timber +import java.io.IOException + /** * the AppGuard object is in charge of contacting the Billing services to retrieve products, @@ -11,6 +14,8 @@ import net.pokeranalytics.android.ui.activity.IAPProducts */ object AppGuard : PurchasesUpdatedListener { + private val BASE_64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VuL5bk2w0FNaZIwE3v2857ZxJlo0epIIsVJfVs7Pqh7zE1JN7uGBgZ/r2s5Rn3o0R1Ycqxp832kYg/B0FvlQJ3Ko6IZkoyfJQ3i1zuAcc7NLMxKUMJY8Mc0U6Go2bevjQ54WkvumIdAFWIlMjyuOOFcSZRZr8V7tlq0SYlenkuellQeHIq3V47M/0jlDrEbCFj59hsukN75eGIiafFAxBYO/8L/flkZLik8YyhV1uZTu+KziA0PsbIvXKyN+gCK9UmrscTyM4+hfmRgb74fro67UsEqq2OvmHFUhubPzCZDElOwPeauUDEGeQjJn43iUHZWIcSEVktVB9cFa/0JwIDAQAB" + /** * The Billing Client making requests with Google Billing services */ @@ -66,7 +71,6 @@ object AppGuard : PurchasesUpdatedListener { } else { this.startConnection(runnable) } - } /** @@ -141,10 +145,9 @@ object AppGuard : PurchasesUpdatedListener { */ private fun handlePurchase(purchase: Purchase) { - val token = purchase.purchaseToken - val oj = purchase.originalJson +// val token = purchase.purchaseToken - if (this.verifyPurchase(purchase)) { + if (this.verifyValidSignature(purchase.originalJson, purchase.signature)) { when (purchase.sku) { IAPProducts.PRO.identifier -> { this.isProUser = true @@ -161,8 +164,13 @@ object AppGuard : PurchasesUpdatedListener { /** * Verifies the validity of a purchase */ - private fun verifyPurchase(purchase: Purchase): Boolean { - return true + private fun verifyValidSignature(signedData: String, signature: String): Boolean { + try { + return Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY, signedData, signature) + } catch (e: IOException) { + Timber.d("Got an exception trying to validate a purchase: $e") + return false + } } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/Security.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/Security.kt new file mode 100644 index 00000000..07676606 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/billing/Security.kt @@ -0,0 +1,127 @@ +package net.pokeranalytics.android.util.billing + +/* + * Copyright (c) 2012 Google Inc. + * + * 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. + */ + + + +import android.text.TextUtils +import android.util.Base64 +import com.android.billingclient.util.BillingHelper +import java.io.IOException +import java.security.* +import java.security.spec.InvalidKeySpecException +import java.security.spec.X509EncodedKeySpec + +/** + * Security-related methods. For a secure implementation, all of this code should be implemented on + * a server that communicates with the application on the device. + */ +object Security { + + private val TAG = "IABUtil/Security" + + private val KEY_FACTORY_ALGORITHM = "RSA" + private val SIGNATURE_ALGORITHM = "SHA1withRSA" + + /** + * Verifies that the data was signed with the given signature, and returns the verified + * purchase. + * @param base64PublicKey the base64-encoded public key to use for verifying. + * @param signedData the signed JSON string (signed, not encrypted) + * @param signature the signature for the data, signed with the private key + * @throws IOException if encoding algorithm is not supported or key specification + * is invalid + */ + @Throws(IOException::class) + fun verifyPurchase( + base64PublicKey: String, signedData: String, + signature: String + ): Boolean { + if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) + || TextUtils.isEmpty(signature) + ) { + BillingHelper.logWarn(TAG, "Purchase verification failed: missing data.") + return false + } + + val key = generatePublicKey(base64PublicKey) + return verify(key, signedData, signature) + } + + /** + * Generates a PublicKey instance from a string containing the Base64-encoded public key. + * + * @param encodedPublicKey Base64-encoded public key + * @throws IOException if encoding algorithm is not supported or key specification + * is invalid + */ + @Throws(IOException::class) + fun generatePublicKey(encodedPublicKey: String): PublicKey { + try { + val decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT) + val keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM) + return keyFactory.generatePublic(X509EncodedKeySpec(decodedKey)) + } catch (e: NoSuchAlgorithmException) { + // "RSA" is guaranteed to be available. + throw RuntimeException(e) + } catch (e: InvalidKeySpecException) { + val msg = "Invalid key specification: $e" + BillingHelper.logWarn(TAG, msg) + throw IOException(msg) + } + + } + + /** + * Verifies that the signature from the server matches the computed signature on the data. + * Returns true if the data is correctly signed. + * + * @param publicKey public key associated with the developer account + * @param signedData signed data from server + * @param signature server signature + * @return true if the data and signature match + */ + fun verify(publicKey: PublicKey, signedData: String, signature: String): Boolean { + val signatureBytes: ByteArray + try { + signatureBytes = Base64.decode(signature, Base64.DEFAULT) + } catch (e: IllegalArgumentException) { + BillingHelper.logWarn(TAG, "Base64 decoding failed.") + return false + } + + try { + val signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM) + signatureAlgorithm.initVerify(publicKey) + signatureAlgorithm.update(signedData.toByteArray()) + if (!signatureAlgorithm.verify(signatureBytes)) { + BillingHelper.logWarn(TAG, "Signature verification failed.") + return false + } + return true + } catch (e: NoSuchAlgorithmException) { + // "RSA" is guaranteed to be available. + throw RuntimeException(e) + } catch (e: InvalidKeyException) { + BillingHelper.logWarn(TAG, "Invalid key specification.") + } catch (e: SignatureException) { + BillingHelper.logWarn(TAG, "Signature exception.") + } + + return false + } +}