From 7de4ad811d44f7f5cffc8fbf2d247ea3d3b89cd7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jun 2020 08:35:53 +0300 Subject: [PATCH] Updated on 2026-08-14 --- tangem-core/build.gradle | 9 + .../src/main/java/com/tangem/TangemSdk.kt | 23 +++ .../commands/common/network/NetworkUtils.kt | 39 +++++ .../common/network/RetrofitBuilder.kt | 30 ++++ .../commands/common/network/TangemApi.kt | 17 ++ .../commands/common/network/TangemService.kt | 22 +++ .../commands/verifycard/VerifyCardCommand.kt | 159 ++++++++++++++++++ .../commands/verifycard/VerifyCardRequest.kt | 60 +++++++ tangem-devkit/src/main/AndroidManifest.xml | 2 + 9 files changed, 361 insertions(+) create mode 100644 tangem-core/src/main/java/com/tangem/commands/common/network/NetworkUtils.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/common/network/RetrofitBuilder.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/common/network/TangemApi.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/common/network/TangemService.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardCommand.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardRequest.kt diff --git a/tangem-core/build.gradle b/tangem-core/build.gradle index bd00611a6a..8b71b5e8bf 100644 --- a/tangem-core/build.gradle +++ b/tangem-core/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'org.jetbrains.dokka' apply plugin: 'com.github.dcendents.android-maven' apply from: '../dependencies.gradle' apply from: '../jitpack.gradle' +apply plugin: 'kotlin-kapt' group = "$jitpackSdk.group" version "$jitpackSdk.version" @@ -22,6 +23,14 @@ dependencies { // misc implementation 'com.google.code.gson:gson:2.8.6' + //network + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' + implementation 'com.squareup.moshi:moshi:1.9.2' + implementation "com.squareup.moshi:moshi-kotlin:1.9.2" + kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2") + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' + // tests testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' testImplementation "com.google.truth:truth:1.0.1" diff --git a/tangem-core/src/main/java/com/tangem/TangemSdk.kt b/tangem-core/src/main/java/com/tangem/TangemSdk.kt index c5ac313d34..6508fb8c3f 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdk.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdk.kt @@ -8,6 +8,8 @@ import com.tangem.commands.personalization.entities.Acquirer import com.tangem.commands.personalization.entities.CardConfig import com.tangem.commands.personalization.entities.Issuer import com.tangem.commands.personalization.entities.Manufacturer +import com.tangem.commands.verifycard.VerifyCardCommand +import com.tangem.commands.verifycard.VerifyCardResponse import com.tangem.common.CompletionResult import com.tangem.common.TerminalKeysService import com.tangem.crypto.CryptoUtils @@ -294,6 +296,27 @@ class TangemSdk( startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback) } + /** + * This method launches a [VerifyCardCommand] on a new thread. + * + * The command to ensures the card has not been counterfeited. + * By using standard challenge-response scheme, the card proves possession of CardPrivateKey + * that corresponds to CardPublicKey returned by [ReadCommand]. Then the data is sent + * to Tangem server to prove that this card was indeed issued by Tangem. + * The online part of the verification is unavailable for DevKit cards. + * + * + * @param cardId CID, Unique Tangem card ID number. + * @param online flag that allows disable online verification + * @param callback is triggered on the completion of the [VerifyCardCommand] and provides + * card response in the form of [VerifyCardResponse] if the task was performed successfully + * or [TangemSdkError] in case of an error. + */ + fun verify(cardId: String? = null, online: Boolean = true, initialMessage: Message? = null, + callback: (result: CompletionResult) -> Unit) { + startSessionWithRunnable(VerifyCardCommand(online), cardId, initialMessage, callback) + } + /** * Command available on SDK cards only * diff --git a/tangem-core/src/main/java/com/tangem/commands/common/network/NetworkUtils.kt b/tangem-core/src/main/java/com/tangem/commands/common/network/NetworkUtils.kt new file mode 100644 index 0000000000..6ff62b1021 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/common/network/NetworkUtils.kt @@ -0,0 +1,39 @@ +package com.tangem.commands.common.network + +import com.tangem.Log +import kotlinx.coroutines.delay +import java.io.IOException + +suspend fun retryIO( + times: Int = 3, + initialDelay: Long = 100, + maxDelay: Long = 1000, + factor: Double = 2.0, + block: suspend () -> T): T +{ + var currentDelay = initialDelay + repeat(times - 1) { + try { + return block() + } catch (e: IOException) { + Log.i("Network", e.localizedMessage) + } + delay(currentDelay) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + } + return block() +} + +sealed class Result { + data class Success(val data: T) : Result() + data class Failure(val error: Throwable?) : Result() +} + +suspend fun performRequest(block: suspend () -> T): Result { + return try { + val result = retryIO { block() } + Result.Success(result) + } catch (exception: Exception) { + Result.Failure(exception) + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/common/network/RetrofitBuilder.kt b/tangem-core/src/main/java/com/tangem/commands/common/network/RetrofitBuilder.kt new file mode 100644 index 0000000000..ffbf990666 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/common/network/RetrofitBuilder.kt @@ -0,0 +1,30 @@ +package com.tangem.commands.common.network + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + + +class ApiTangem { + companion object { + const val TANGEM_ENDPOINT: String = "https://verify.tangem.com/" + + const val VERIFY = "verify" + const val VERIFY_AND_GET_INFO = "card/verify-and-get-info" + const val ARTWORK = "card/artwork" + } + +} + +private val moshi: Moshi by lazy { + Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() +} + +fun createRetrofitInstance(baseUrl: String): Retrofit = + Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/common/network/TangemApi.kt b/tangem-core/src/main/java/com/tangem/commands/common/network/TangemApi.kt new file mode 100644 index 0000000000..75507687c6 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/common/network/TangemApi.kt @@ -0,0 +1,17 @@ +package com.tangem.commands.common.network + + +import com.tangem.commands.common.network.ApiTangem.Companion.VERIFY_AND_GET_INFO +import com.tangem.commands.verifycard.CardVerifyAndGetInfo +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +interface TangemApi { + + @Headers("Content-Type: application/json") + @POST(VERIFY_AND_GET_INFO) + suspend fun getCardVerifyAndGetInfo(@Body requestBody: CardVerifyAndGetInfo.Request): CardVerifyAndGetInfo.Response + + +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/common/network/TangemService.kt b/tangem-core/src/main/java/com/tangem/commands/common/network/TangemService.kt new file mode 100644 index 0000000000..295536233d --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/common/network/TangemService.kt @@ -0,0 +1,22 @@ +package com.tangem.commands.common.network + +import com.tangem.commands.verifycard.CardVerifyAndGetInfo + +class TangemService { + + private val tangemApi: TangemApi by lazy { + createRetrofitInstance(ApiTangem.TANGEM_ENDPOINT).create(TangemApi::class.java) + } + + suspend fun verifyAndGetInfo( + cardId: String, + cardPublicKey: String + ): Result { + val requestsBody = CardVerifyAndGetInfo.Request() + requestsBody.requests = + listOf(CardVerifyAndGetInfo.Request.Item(cardId, cardPublicKey)) + + return performRequest { tangemApi.getCardVerifyAndGetInfo(requestsBody) } + } + +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardCommand.kt b/tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardCommand.kt new file mode 100644 index 0000000000..1eb97c4aa9 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardCommand.kt @@ -0,0 +1,159 @@ +package com.tangem.commands.verifycard + +import com.tangem.CardSession +import com.tangem.SessionEnvironment +import com.tangem.TangemSdkError +import com.tangem.commands.Card +import com.tangem.commands.CardStatus +import com.tangem.commands.Command +import com.tangem.commands.CommandResponse +import com.tangem.commands.common.network.Result +import com.tangem.commands.common.network.TangemService +import com.tangem.common.CompletionResult +import com.tangem.common.apdu.CommandApdu +import com.tangem.common.apdu.Instruction +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.extensions.CardType +import com.tangem.common.extensions.getType +import com.tangem.common.extensions.toHexString +import com.tangem.common.tlv.TlvBuilder +import com.tangem.common.tlv.TlvDecoder +import com.tangem.common.tlv.TlvTag +import com.tangem.crypto.CryptoUtils +import kotlinx.coroutines.launch + +class VerifyCardResponse( + val cardId: String, + val verificationState: VerifyCardState? = null, + val artworkInfo: ArtworkInfo? = null, + internal val salt: ByteArray, + internal val cardSignature: ByteArray +) : CommandResponse { + + fun verify(publicKey: ByteArray, challenge: ByteArray): Boolean { + return CryptoUtils.verify( + publicKey, + challenge + salt, + cardSignature + ) + } +} + +enum class VerifyCardState { + VerifiedOnline, + VerifiedOffline, +} + +class VerifyCardCommand(private val onlineVerification: Boolean) : Command() { + + private val challenge = CryptoUtils.generateRandomBytes(16) + private val tangemService = TangemService() + + override fun run( + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + val card = session.environment.card + val cardPublicKey = card?.cardPublicKey + if (cardPublicKey == null) { + callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) + return + } + super.run(session) { result -> + when (result) { + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + is CompletionResult.Success -> { + val response = result.data + val verified = response.verify(cardPublicKey, challenge) + if (!verified) { + callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) + return@run + } + if (!onlineVerification || card.getType() != CardType.Release) { + callback( + CompletionResult.Success( + VerifyCardResponse( + response.cardId, VerifyCardState.VerifiedOffline, null, + response.salt, response.cardSignature + ) + ) + ) + } else { + verify(result.data, card.cardId, cardPublicKey, session, callback) + } + } + } + } + } + + private fun verify( + response: VerifyCardResponse, cardId: String, cardPublicKey: ByteArray, + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + session.scope.launch { + val result = tangemService.verifyAndGetInfo(cardId, cardPublicKey.toHexString()) + + when (result) { + is Result.Success -> { + if (result.data.results?.firstOrNull()?.passed == true) { + callback( + CompletionResult.Success( + VerifyCardResponse( + response.cardId, VerifyCardState.VerifiedOnline, response.artworkInfo, + response.salt, response.cardSignature + ) + ) + ) + } else { + callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) + } + } + is Result.Failure -> { + callback( + CompletionResult.Success( + VerifyCardResponse( + response.cardId, VerifyCardState.VerifiedOffline, null, + response.salt, response.cardSignature + ) + ) + ) + } + } + } + } + + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() + } + if (card.isActivated) { + return TangemSdkError.NotActivated() + } + return null + } + + override fun serialize(environment: SessionEnvironment): CommandApdu { + val tlvBuilder = TlvBuilder() + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.CardId, environment.card?.cardId) + tlvBuilder.append(TlvTag.Challenge, challenge) + return CommandApdu(Instruction.VerifyCard, tlvBuilder.serialize()) + } + + override fun deserialize( + environment: SessionEnvironment, + apdu: ResponseApdu + ): VerifyCardResponse { + val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed() + + val decoder = TlvDecoder(tlvData) + return VerifyCardResponse( + cardId = decoder.decode(TlvTag.CardId), + salt = decoder.decode(TlvTag.Salt), + cardSignature = decoder.decode(TlvTag.CardSignature) + ) + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardRequest.kt b/tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardRequest.kt new file mode 100644 index 0000000000..8bc7efd052 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/verifycard/VerifyCardRequest.kt @@ -0,0 +1,60 @@ +package com.tangem.commands.verifycard + +import com.squareup.moshi.JsonClass +import java.text.SimpleDateFormat +import java.util.* + + +class CardVerifyAndGetInfo { + + @JsonClass(generateAdapter = true) + data class Request( + var requests: List? = null + ) { + @JsonClass(generateAdapter = true) + data class Item( + var CID: String = "", + var publicKey: String = "" + ) + } + + @JsonClass(generateAdapter = true) + data class Response( + var results: List? = null + ) { + @JsonClass(generateAdapter = true) + data class Item( + var error: String? = null, + var CID: String = "", + var passed: Boolean = false, + var batch: String = "", + var artwork: ArtworkInfo? = null, + var substitution: SubstitutionInfo? = null + ) { + + @JsonClass(generateAdapter = true) + data class SubstitutionInfo( + var data: String? = null, + var signature: String? = null + ) + + } + } +} + +@JsonClass(generateAdapter = true) +data class ArtworkInfo( + var id: String = "", + var hash: String = "", + var date: String = "" +) { + fun getUpdateDate(): Date? { + return try { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US).parse(date) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + +} \ No newline at end of file diff --git a/tangem-devkit/src/main/AndroidManifest.xml b/tangem-devkit/src/main/AndroidManifest.xml index e0749468f0..b74aaf74df 100644 --- a/tangem-devkit/src/main/AndroidManifest.xml +++ b/tangem-devkit/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ android:name="android.hardware.nfc" android:required="true" /> + +