Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-21 17:16:08 +03:00
parent 2709b9d5b4
commit 9a95b49b4a
13 changed files with 103 additions and 87 deletions

View file

@ -33,22 +33,20 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens)
secureStorage.store(
json.encodeToByteArray(throwOnInvalidSequence = true),
VISA_AUTH_TOKENS_KEY,
createKey(cardId),
)
}
override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) {
secureStorage.get(VISA_AUTH_TOKENS_KEY)
override suspend fun get(cardId: String): VisaAuthTokens? = withContext(dispatcherProvider.io) {
secureStorage.get(createKey(cardId))
?.decodeToString(throwOnInvalidSequence = true)
?.let(tokensAdapter::fromJson)
}
private companion object {
const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens"
}
private fun createKey(cardId: String): String = "visa_auth_tokens_$cardId"
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.tasks.visa
import com.reown.util.hexToBytes
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
@ -36,9 +37,15 @@ class VisaCardActivationTask @AssistedInject constructor(
private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) : CardSessionRunnable<VisaCardActivationResponse> {
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val card: Card,
val session: CardSession,
)
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
coroutineScope.launch {
callback(runSuspend(session))
@ -52,20 +59,27 @@ class VisaCardActivationTask @AssistedInject constructor(
return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message))
}
val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
val context = SessionContext(
visaActivationRepository = visaActivationRepository,
card = card,
session = session,
)
return if (challengeToSign != null) {
signAuthorizationChallenge(session, challengeToSign)
context.signAuthorizationChallenge(challengeToSign)
} else {
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
signOrder(session, activationOrder)
context.signOrder(activationOrder)
}
}
private suspend fun signAuthorizationChallenge(
session: CardSession,
private suspend fun SessionContext.signAuthorizationChallenge(
challengeToSign: VisaAuthChallenge.Card,
): CompletionResult<VisaCardActivationResponse> {
val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes())
@ -78,7 +92,6 @@ class VisaCardActivationTask @AssistedInject constructor(
return when (result) {
is CompletionResult.Success -> {
processSignedAuthorizationChallenge(
session = session,
signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(),
salt = result.data.salt.toHexString(),
@ -91,8 +104,7 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
private suspend fun processSignedAuthorizationChallenge(
session: CardSession,
private suspend fun SessionContext.processSignedAuthorizationChallenge(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
return coroutineScope {
@ -107,12 +119,14 @@ class VisaCardActivationTask @AssistedInject constructor(
otpTaskDeferred.await()
signOrder(session, order)
signOrder(order)
}
}
@Throws(TangemSdkError::class)
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder {
private suspend fun SessionContext.getActivationOrderToSign(
signedChallenge: VisaAuthSignedChallenge,
): ActivationOrder {
val tokens = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge)
}.getOrElse {
@ -121,7 +135,7 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
visaAuthTokenStorage.store(tokens)
visaAuthTokenStorage.store(card.cardId, tokens)
return visaActivationRepository.getActivationOrderToSign()
}
@ -180,11 +194,7 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
private suspend fun signOrder(
session: CardSession,
order: ActivationOrder,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
private suspend fun SessionContext.signOrder(order: ActivationOrder): CompletionResult<VisaCardActivationResponse> {
val wallet =
card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey)
@ -197,7 +207,6 @@ class VisaCardActivationTask @AssistedInject constructor(
return when (result) {
is CompletionResult.Success -> {
handleSignedOrder(
session = session,
activationOrder = order,
response = result.data,
)
@ -208,13 +217,10 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
private suspend fun handleSignedOrder(
session: CardSession,
private suspend fun SessionContext.handleSignedOrder(
activationOrder: ActivationOrder,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val signedOrder = SignedActivationOrder(
activationOrder = activationOrder,
signature = response.signature.toHexString(),
@ -232,9 +238,7 @@ class VisaCardActivationTask @AssistedInject constructor(
return setupAccessCode(session).map { activationResponse }
}
private suspend fun setupAccessCode(session: CardSession): CompletionResult<Unit> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
private suspend fun SessionContext.setupAccessCode(session: CardSession): CompletionResult<Unit> {
if (card.isAccessCodeSet) {
return CompletionResult.Success(Unit)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.visa
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.core.CardSession
import com.tangem.common.core.TangemSdkError
@ -24,10 +25,16 @@ import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
) {
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val card: Card,
val session: CardSession,
)
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan")
@ -36,6 +43,14 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
}
val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId)
val context = SessionContext(
visaActivationRepository = visaActivationRepository,
card = card,
session = session,
)
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
val activationInput =
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
@ -43,13 +58,10 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(activationStatus)
}
return deriveKey(wallet, session)
return context.deriveKey(wallet)
}
private suspend fun deriveKey(
wallet: CardWallet,
session: CardSession,
): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult<VisaCardActivationStatus> {
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
Timber.e("Failed to create derivation path while first scan")
@ -64,17 +76,16 @@ internal class VisaCardScanHandler @Inject constructor(
continuation.resume(result)
}
}
return handleDerivationResponse(derivationTaskResult, session)
return handleDerivationResponse(derivationTaskResult)
}
private suspend fun handleDerivationResponse(
private suspend fun SessionContext.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)
handleWalletAuthorization()
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
@ -82,9 +93,8 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.handleWalletAuthorization(): 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")
@ -122,14 +132,12 @@ internal class VisaCardScanHandler @Inject constructor(
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()),
)
@ -141,27 +149,31 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
private suspend fun handleWalletAuthorizationTokens(
session: CardSession,
private suspend fun SessionContext.handleWalletAuthorizationTokens(
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
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)
return handleCardAuthorization()
}
visaAuthTokenStorage.store(authorizationTokensResponse)
visaAuthTokenStorage.store(
cardId = card.cardId,
tokens = authorizationTokensResponse,
)
Timber.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.handleCardAuthorization(): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
@ -178,7 +190,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge)
val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> {
@ -210,7 +222,10 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
visaAuthTokenStorage.store(authorizationTokensResponse)
visaAuthTokenStorage.store(
cardId = card.cardId,
tokens = authorizationTokensResponse,
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
@ -239,11 +254,10 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
private suspend fun signChallengeWithWallet(
private suspend fun SessionContext.signChallengeWithWallet(
publicKey: ByteArray,
derivationPath: DerivationPath,
nonce: String,
session: CardSession,
): CompletionResult<SignHashResponse> {
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
val result = suspendCancellableCoroutine {
@ -262,8 +276,7 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
private suspend fun signChallengeWithCard(
session: CardSession,
private suspend fun SessionContext.signChallengeWithCard(
challenge: String,
): CompletionResult<AttestCardKeyResponse> {
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())

View file

@ -8,7 +8,7 @@ internal class DefaultVisaAuthProvider @Inject constructor(
private val authStorage: VisaAuthTokenStorage,
) : TangemVisaAuthProvider {
override suspend fun getAuthHeader(): String {
return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!"
override suspend fun getAuthHeader(cardId: String): String {
return authStorage.get(cardId)?.accessToken?.let { "Bearer $it" } ?: "Error in the app!"
}
}