mirror of https://github.com/tasks/tasks
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.
138 lines
5.9 KiB
Kotlin
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
|
|
}
|
|
} |