Add server public key check before purchases

This commit is contained in:
Jonas Lochmann 2020-06-08 02:00:00 +02:00
parent 5a17045551
commit 13c97bc41e
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
7 changed files with 57 additions and 34 deletions

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,17 +15,41 @@
*/ */
package io.timelimit.android.sync.network package io.timelimit.android.sync.network
enum class CanDoPurchaseStatus { import android.util.Base64
Yes, NotDueToOldPurchase, NoForUnknownReason import android.util.JsonReader
sealed class CanDoPurchaseStatus {
class Yes(val publicKey: ByteArray?): CanDoPurchaseStatus()
object NoForUnknownReason: CanDoPurchaseStatus()
object NotDueToOldPurchase: CanDoPurchaseStatus()
} }
object CanDoPurchaseParser { object CanDoPurchaseParser {
private const val CAN_DO_PURCHASE = "canDoPurchase"
private const val GPLAY_PUBLIC_KEY = "googlePlayPublicKey"
private const val YES = "yes" private const val YES = "yes"
private const val NO_DUE_TO_OLD_PURCHASE = "no due to old purchase" private const val NO_DUE_TO_OLD_PURCHASE = "no due to old purchase"
fun parse(value: String) = when(value) { fun parse(reader: JsonReader): CanDoPurchaseStatus {
YES -> CanDoPurchaseStatus.Yes reader.beginObject()
NO_DUE_TO_OLD_PURCHASE -> CanDoPurchaseStatus.NotDueToOldPurchase
else -> CanDoPurchaseStatus.NoForUnknownReason var canDoPurchaseStatus: String? = null
var publicKey: ByteArray? = null
while (reader.hasNext()) {
when (reader.nextName()) {
CAN_DO_PURCHASE -> canDoPurchaseStatus = reader.nextString()
GPLAY_PUBLIC_KEY -> publicKey = Base64.decode(reader.nextString(), 0)
else -> reader.skipValue()
}
}
reader.endObject()
return when (canDoPurchaseStatus!!) {
YES -> CanDoPurchaseStatus.Yes(publicKey)
NO_DUE_TO_OLD_PURCHASE -> CanDoPurchaseStatus.NotDueToOldPurchase
else -> CanDoPurchaseStatus.NoForUnknownReason
}
} }
} }

View file

@ -476,18 +476,8 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
return Threads.network.executeAndWait { return Threads.network.executeAndWait {
val reader = JsonReader(response.body()!!.charStream()) val reader = JsonReader(response.body()!!.charStream())
var status: CanDoPurchaseStatus? = null
reader.beginObject() CanDoPurchaseParser.parse(reader)
while (reader.hasNext()) {
when (reader.nextName()) {
"canDoPurchase" -> status = CanDoPurchaseParser.parse(reader.nextString())
else -> reader.skipValue()
}
}
reader.endObject()
status!!
} }
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -194,7 +194,7 @@ class ActivityPurchaseModel(application: Application): AndroidViewModel(applicat
return@runAsync return@runAsync
} }
if (server.api.canDoPurchase(server.deviceAuthToken) != CanDoPurchaseStatus.Yes) { if (!(server.api.canDoPurchase(server.deviceAuthToken) is CanDoPurchaseStatus.Yes)) {
throw IOException("can not do purchase right now") throw IOException("can not do purchase right now")
} }
} }

View file

@ -90,6 +90,7 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
PurchaseFragmentNetworkError -> getString(R.string.error_network) PurchaseFragmentNetworkError -> getString(R.string.error_network)
PurchaseFragmentExistingPaymentError -> getString(R.string.purchase_error_existing_payment) PurchaseFragmentExistingPaymentError -> getString(R.string.purchase_error_existing_payment)
PurchaseFragmentServerRejectedError -> getString(R.string.purchase_error_server_rejected) PurchaseFragmentServerRejectedError -> getString(R.string.purchase_error_server_rejected)
PurchaseFragmentServerHasDifferentPublicKey -> getString(R.string.purchase_error_server_different_key)
} }
binding.showRetryButton = when (fragmentStatus) { binding.showRetryButton = when (fragmentStatus) {

View file

@ -16,6 +16,7 @@
package io.timelimit.android.ui.payment package io.timelimit.android.ui.payment
import android.app.Application import android.app.Application
import android.util.Base64
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
@ -65,22 +66,26 @@ class PurchaseModel(application: Application): AndroidViewModel(application) {
else else
CanDoPurchaseStatus.NoForUnknownReason CanDoPurchaseStatus.NoForUnknownReason
if (canDoPurchase == CanDoPurchaseStatus.Yes) { if (canDoPurchase is CanDoPurchaseStatus.Yes) {
val checkout = Checkout.forApplication(application.billing) if (canDoPurchase.publicKey?.equals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
statusInternal.value = PurchaseFragmentServerHasDifferentPublicKey
} else {
val checkout = Checkout.forApplication(application.billing)
checkout.startAsync().use { checkout.startAsync().use {
if (!it.billingSupported) { if (!it.billingSupported) {
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
} else { } else {
val skus = it.requests.getSkusAsync( val skus = it.requests.getSkusAsync(
ProductTypes.IN_APP, ProductTypes.IN_APP,
PurchaseIds.BUY_SKUS PurchaseIds.BUY_SKUS
) )
statusInternal.value = PurchaseFragmentReady( statusInternal.value = PurchaseFragmentReady(
monthPrice = skus.getSku(PurchaseIds.SKU_MONTH)?.price.toString(), monthPrice = skus.getSku(PurchaseIds.SKU_MONTH)?.price.toString(),
yearPrice = skus.getSku(PurchaseIds.SKU_YEAR)?.price.toString() yearPrice = skus.getSku(PurchaseIds.SKU_YEAR)?.price.toString()
) )
}
} }
} }
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) { } else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
@ -110,3 +115,4 @@ object PurchaseFragmentErrorBillingNotSupportedByAppVariant: PurchaseFragmentUnr
object PurchaseFragmentNetworkError: PurchaseFragmentRecoverableError() object PurchaseFragmentNetworkError: PurchaseFragmentRecoverableError()
object PurchaseFragmentExistingPaymentError: PurchaseFragmentUnrecoverableError() object PurchaseFragmentExistingPaymentError: PurchaseFragmentUnrecoverableError()
object PurchaseFragmentServerRejectedError: PurchaseFragmentUnrecoverableError() object PurchaseFragmentServerRejectedError: PurchaseFragmentUnrecoverableError()
object PurchaseFragmentServerHasDifferentPublicKey: PurchaseFragmentUnrecoverableError()

View file

@ -41,6 +41,7 @@
Sie haben bereits bezahlt. Sie können die Vollversion erst kurz vor dem Ablaufen (oder danach) verlängern Sie haben bereits bezahlt. Sie können die Vollversion erst kurz vor dem Ablaufen (oder danach) verlängern
</string> </string>
<string name="purchase_error_server_rejected">Der Server akzeptiert momentan keine Bezahlungen</string> <string name="purchase_error_server_rejected">Der Server akzeptiert momentan keine Bezahlungen</string>
<string name="purchase_error_server_different_key">Der Server unterstützt Bezahlungen, aber nicht über diesen App-Build</string>
<string name="purchase_done_title">Bezahlung abgeschlossen</string> <string name="purchase_done_title">Bezahlung abgeschlossen</string>
<string name="purchase_done_text">Betätigen Sie die Zurück-Taste, um zur App zurückzukehren</string> <string name="purchase_done_text">Betätigen Sie die Zurück-Taste, um zur App zurückzukehren</string>

View file

@ -41,6 +41,7 @@
You can only extend the premium version if it expired or shortly before that. You can only extend the premium version if it expired or shortly before that.
</string> </string>
<string name="purchase_error_server_rejected">The server does not accept payments currently</string> <string name="purchase_error_server_rejected">The server does not accept payments currently</string>
<string name="purchase_error_server_different_key">The server does support payments, but not using this app build</string>
<string name="purchase_done_title">Purchase succeeded</string> <string name="purchase_done_title">Purchase succeeded</string>
<string name="purchase_done_text">Press back to go back to the App</string> <string name="purchase_done_text">Press back to go back to the App</string>