Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-18 08:35:53 +03:00
parent 19d44f47d4
commit 7de4ad811d
9 changed files with 361 additions and 0 deletions

View file

@ -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"

View file

@ -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<VerifyCardResponse>) -> Unit) {
startSessionWithRunnable(VerifyCardCommand(online), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*

View file

@ -0,0 +1,39 @@
package com.tangem.commands.common.network
import com.tangem.Log
import kotlinx.coroutines.delay
import java.io.IOException
suspend fun <T> 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<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
suspend fun <T>performRequest(block: suspend () -> T): Result<T> {
return try {
val result = retryIO { block() }
Result.Success(result)
} catch (exception: Exception) {
Result.Failure(exception)
}
}

View file

@ -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()

View file

@ -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
}

View file

@ -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<CardVerifyAndGetInfo.Response> {
val requestsBody = CardVerifyAndGetInfo.Request()
requestsBody.requests =
listOf(CardVerifyAndGetInfo.Request.Item(cardId, cardPublicKey))
return performRequest { tangemApi.getCardVerifyAndGetInfo(requestsBody) }
}
}

View file

@ -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<VerifyCardResponse>() {
private val challenge = CryptoUtils.generateRandomBytes(16)
private val tangemService = TangemService()
override fun run(
session: CardSession,
callback: (result: CompletionResult<VerifyCardResponse>) -> 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<VerifyCardResponse>) -> 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)
)
}
}

View file

@ -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<Item>? = null
) {
@JsonClass(generateAdapter = true)
data class Item(
var CID: String = "",
var publicKey: String = ""
)
}
@JsonClass(generateAdapter = true)
data class Response(
var results: List<Item>? = 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
}
}
}

View file

@ -6,6 +6,8 @@
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<application