You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/app/src/googleplay/java/org/tasks/billing/Security.kt

138 lines
5.9 KiB
Kotlin

/**
* Copyright (C) 2018 Google Inc. All Rights Reserved.
*
* 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 org.tasks.billing
/**
* This class is an addendum. It shouldn't really be here: it should be on the secure server. But if
* a secure server does not exist, it's still good to check the signature of the purchases coming
* from Google Play. At the very least, it will combat man-in-the-middle attacks. Putting it
* on the server would provide the additional protection against hackers who may
* decompile/rebuild the app.
*
* Sigh... All sorts of attacks can befall your app, website, or platform. So when it comes to
* implementing security measures, you have to be realistic and judicious so that user experience
* does not suffer needlessly. And you should analyze that the money you will save (minus cost of labor)
* by implementing security measure X is greater than the money you would lose if you don't
* implement X. Talk to a UX designer if you find yourself obsessing over security.
*
* The good news is, in implementing BillingRepository, a number of measures is taken to help
* prevent fraudulent activities in your app. We don't just focus on tech savvy hackers, but also
* on fraudulent users who may want to exploit loopholes. Just to name an obvious case:
* triangulation using Google Play, your secure server, and a local cache helps against non-techie
* frauds.
*/
import android.text.TextUtils
import android.util.Base64
import timber.log.Timber
import java.io.IOException
import java.security.InvalidKeyException
import java.security.KeyFactory
import java.security.NoSuchAlgorithmException
import java.security.PublicKey
import java.security.Signature
import java.security.SignatureException
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 const val KEY_FACTORY_ALGORITHM = "RSA"
private const val SIGNATURE_ALGORITHM = "SHA1withRSA"
/**
* Verifies that the data was signed with the given signature
*
* @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))
) {
Timber.w("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)
private 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"
Timber.w(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
*/
private fun verify(publicKey: PublicKey, signedData: String, signature: String): Boolean {
val signatureBytes: ByteArray
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT)
} catch (e: IllegalArgumentException) {
Timber.w("Base64 decoding failed.")
return false
}
try {
val signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM)
signatureAlgorithm.initVerify(publicKey)
signatureAlgorithm.update(signedData.toByteArray())
if (!signatureAlgorithm.verify(signatureBytes)) {
Timber.w("Signature verification failed...")
return false
}
return true
} catch (e: NoSuchAlgorithmException) {
// "RSA" is guaranteed to be available.
throw RuntimeException(e)
} catch (e: InvalidKeyException) {
Timber.w("Invalid key specification.")
} catch (e: SignatureException) {
Timber.w("Signature exception.")
}
return false
}
}