Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-14 11:04:06 +03:00
parent 6111067060
commit 8ca71f0788
25 changed files with 554 additions and 20 deletions

View file

@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,18 +16,23 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TangemSdkManagerModule {
internal class TangemSdkManagerModule {
@Provides
@Singleton
fun provideTangemSdkManager(
@ApplicationContext context: Context,
cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources)
} else {
DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources)
DefaultTangemSdkManager(
cardSdkConfigRepository = cardSdkConfigRepository,
resources = context.resources,
visaCardScanHandler = visaCardScanHandler,
)
}
}
}

View file

@ -43,19 +43,18 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@Suppress("TooManyFunctions", "LargeClass")
class DefaultTangemSdkManager(
internal class DefaultTangemSdkManager(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler,
) : TangemSdkManager {
private val awaitInitializationMutex = Mutex()
@ -127,15 +126,19 @@ class DefaultTangemSdkManager(
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<ScanResponse> {
val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
card = null,
derivationsFinder = derivationsFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
),
cardId = cardId,
initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
card = null,
derivationsFinder = derivationsFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,
),
cardId = cardId,
initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
}
}
override suspend fun createProductWallet(

View file

@ -9,6 +9,7 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.deserialization.WalletDataDeserializer
import com.tangem.common.extensions.*
import com.tangem.common.map
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
@ -22,6 +23,7 @@ import com.tangem.domain.common.TapWorkarounds.isVisa
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
@ -35,16 +37,20 @@ import com.tangem.operations.files.ReadFilesTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.visa.VisaCardScanHandler
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.collections.set
internal class ScanProductTask(
private val card: Card?,
private val derivationsFinder: DerivationsFinder?,
private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?,
override val allowsRequestAccessCodeFromRepository: Boolean = false,
) : CardSessionRunnable<ScanResponse> {
@ -61,6 +67,16 @@ internal class ScanProductTask(
return
}
if (VisaUtilities.isVisaCard(cardDto)) {
readVisaCard(
session = session,
cardDto = cardDto,
scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
callback = callback,
)
return
}
val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(derivationsFinder)
@ -96,6 +112,43 @@ internal class ScanProductTask(
}
return null
}
private fun readVisaCard(
session: CardSession,
cardDto: CardDTO,
scanWalletProcessor: ScanWalletProcessor,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
visaCardScanHandler ?: run {
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
return
}
visaCoroutineScope ?: run {
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
return
}
visaCoroutineScope.launch {
when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) {
is CompletionResult.Success -> {
scanWalletProcessor.proceed(
card = cardDto,
session = session,
) { scanResponseResult ->
callback(
scanResponseResult.map { scanResponse ->
scanResponse.copy(visaCardActivationStatus = result.data)
},
)
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}
private class ScanWalletProcessor(

View file

@ -23,8 +23,12 @@ class FinalizeTwinTask(
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success ->
ScanProductTask(readResult.data, derivationsFinder = null)
.run(session, callback)
ScanProductTask(
readResult.data,
derivationsFinder = null,
visaCardScanHandler = null,
visaCoroutineScope = null,
).run(session, callback)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(readResult.error))
}

View file

@ -8,6 +8,7 @@ import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
@ -60,6 +61,7 @@ internal object UserWalletsListManagerModule {
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer)
.add(KotlinJsonAdapterFactory())
.build()

View file

@ -0,0 +1,256 @@
package com.tangem.tap.domain.visa
import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.toSignedChallenge
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository,
) {
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan")
val card = session.environment.card ?: run {
Timber.e("Card is null")
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
}
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput)
return CompletionResult.Success(activationStatus)
}
return deriveKey(wallet, session)
}
private suspend fun deriveKey(
wallet: CardWallet,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while first scan")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
)
}
val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath)
val derivationTaskResult = suspendCancellableCoroutine { continuation ->
derivationTask.run(session) { result ->
continuation.resume(result)
}
}
return handleDerivationResponse(derivationTaskResult, session)
}
private suspend fun handleDerivationResponse(
result: CompletionResult<ExtendedPublicKey>,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
return when (result) {
is CompletionResult.Success -> {
Timber.i("Start task for loading challenge for Visa wallet")
handleWalletAuthorization(session)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet")
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while handling wallet authorization")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
)
}
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
)
}
val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
)
}
Timber.i("Requesting challenge for wallet authorization")
// Will be changed later after backend implementation
val challengeResponse = runCatching {
visaAuthRepository.getCustomerWalletAuthChallenge(
cardId = card.cardId,
walletPublicKey = extendedPublicKey.publicKey.toHexString(),
)
}.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
}
val signChallengeResult = signChallengeWithWallet(
publicKey = wallet.publicKey,
derivationPath = derivationPath,
nonce = challengeResponse.challenge,
session = session,
)
return when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
session = session,
signedChallenge = challengeResponse
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
)
}
is CompletionResult.Failure -> {
Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}")
CompletionResult.Failure(signChallengeResult.error)
}
}
}
private suspend fun handleWalletAuthorizationTokens(
session: CardSession,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
}.getOrElse {
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization(session)
}
Timber.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
val challengeResponse = runCatching {
visaAuthRepository.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
)
}.getOrElse {
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
}
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenged signed.")
signChallengeResult.data
}
is CompletionResult.Failure -> {
Timber.e(
"Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}",
)
return CompletionResult.Failure(signChallengeResult.error)
}
}
@Suppress("UnusedPrivateMember")
val authorizationTokensResponse = runCatching {
visaAuthRepository.getAccessTokens(
signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
salt = attestCardKeyResponse.salt.toHexString(),
),
)
}.getOrElse {
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
return CompletionResult.Failure(
TangemSdkError.Underlying(
customMessage = it.message ?: "Unknown error",
),
)
}
TODO() // implement card activation status handling ([REDACTED_TASK_KEY])
}
private suspend fun signChallengeWithWallet(
publicKey: ByteArray,
derivationPath: DerivationPath,
nonce: String,
session: CardSession,
): CompletionResult<SignHashResponse> {
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
val result = suspendCancellableCoroutine {
signHashCommand.run(session) { result ->
it.resume(result)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(result.data)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun signChallengeWithCard(
session: CardSession,
challenge: String,
): CompletionResult<AttestCardKeyResponse> {
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())
val result = suspendCancellableCoroutine { continuation ->
signHashCommand.run(session) { result ->
continuation.resume(result)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(result.data)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.domain.visa
private const val COMMON_DESCRIPTION = "Error occurred. Please contact support."
internal enum class VisaCardScanHandlerError(val errorDescription: String) {
FailedToCreateDerivationPath(COMMON_DESCRIPTION),
FailedToFindWallet(COMMON_DESCRIPTION),
FailedToFindDerivedWalletKey(COMMON_DESCRIPTION),
}