Updated on 2026-08-14
This commit is contained in:
parent
80f829a2aa
commit
d468010146
16 changed files with 457 additions and 58 deletions
|
|
@ -2,7 +2,13 @@ package com.tangem.data.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.pay.util.RainCryptoUtil
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
|
|
@ -13,6 +19,9 @@ private const val TAG = "TangemPay: CardDetailsRepository"
|
|||
internal class DefaultCardDetailsRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
private val rainCryptoUtil: RainCryptoUtil,
|
||||
) : CardDetailsRepository {
|
||||
|
||||
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
|
||||
|
|
@ -28,6 +37,47 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails> {
|
||||
TODO("[REDACTED_TASK_KEY] add reveal card details")
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyHex, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
authHeader = authHeader,
|
||||
body = CardDetailsRequest(sessionId = sessionId),
|
||||
)
|
||||
}.result ?: error("Cannot reveal card details")
|
||||
|
||||
val pan = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.pan.secret,
|
||||
base64Iv = result.pan.iv,
|
||||
secretKeyHex = secretKeyHex,
|
||||
)
|
||||
|
||||
val cvv = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.cvv.secret,
|
||||
base64Iv = result.cvv.iv,
|
||||
secretKeyHex = secretKeyHex,
|
||||
)
|
||||
|
||||
TangemPayCardDetails(
|
||||
pan = pan,
|
||||
cvv = cvv,
|
||||
expirationYear = result.expirationYear,
|
||||
expirationMonth = result.expirationMonth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPublicKeyBase64(): String {
|
||||
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
|
||||
return when (env) {
|
||||
ApiEnvironment.DEV,
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.MOCK,
|
||||
-> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.dev
|
||||
ApiEnvironment.PROD -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.prod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -58,8 +59,15 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
val result = requestBlock()
|
||||
Either.Right(result)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e("$tag: $exception")
|
||||
Either.Left(mapError(exception))
|
||||
when (exception) {
|
||||
is CancellationException -> {
|
||||
throw exception
|
||||
}
|
||||
else -> {
|
||||
Timber.tag(tag).e(exception)
|
||||
Either.Left(mapError(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import android.util.Base64
|
||||
import com.tangem.utils.extensions.hexToBytes
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.KeyFactory
|
||||
import java.security.SecureRandom
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_LENGTH_BYTES = 16
|
||||
private const val TAG_LENGTH_BYTES = 16
|
||||
private const val BITS_PER_BYTE = 8
|
||||
private const val RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
|
||||
private const val AES_TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val AES_ALGORITHM = "AES"
|
||||
private const val RSA_ALGORITHM = "RSA"
|
||||
|
||||
internal class RainCryptoUtil @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend fun generateSecretKeyAndSessionId(publicKeyBase64: String): Pair<String, String> =
|
||||
withContext(dispatchers.default) {
|
||||
val secretKeyHex = ByteArray(KEY_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }.toHexString()
|
||||
val sessionId = generateSessionId(publicKeyBase64, secretKeyHex)
|
||||
secretKeyHex to sessionId
|
||||
}
|
||||
|
||||
suspend fun decryptSecret(base64Secret: String, base64Iv: String, secretKeyHex: String): String =
|
||||
withContext(dispatchers.default) {
|
||||
val cipherTextBytes = Base64.decode(base64Secret, Base64.NO_WRAP)
|
||||
if (cipherTextBytes.size < TAG_LENGTH_BYTES) error("Cipher text too short")
|
||||
|
||||
val initializationVectorBytes = Base64.decode(base64Iv, Base64.NO_WRAP)
|
||||
val secretKeyBytes = secretKeyHex.hexToBytes()
|
||||
|
||||
val aesSecretKey = SecretKeySpec(secretKeyBytes, AES_ALGORITHM)
|
||||
val gcmParameterSpec = GCMParameterSpec(TAG_LENGTH_BYTES * BITS_PER_BYTE, initializationVectorBytes)
|
||||
|
||||
val aesCipher = Cipher.getInstance(AES_TRANSFORMATION)
|
||||
aesCipher.init(Cipher.DECRYPT_MODE, aesSecretKey, gcmParameterSpec)
|
||||
|
||||
val plaintextBytes = aesCipher.doFinal(cipherTextBytes)
|
||||
plaintextBytes.toString(StandardCharsets.UTF_8).trim().ifEmpty { error("Invalid decrypted data") }
|
||||
}
|
||||
|
||||
private fun generateSessionId(publicKeyBase64: String, secretKeyHex: String): String {
|
||||
val secretKeyBytes = secretKeyHex.hexToBytes()
|
||||
val publicKeyDerBytes = Base64.decode(publicKeyBase64, Base64.NO_WRAP)
|
||||
val publicKeySpec = X509EncodedKeySpec(publicKeyDerBytes)
|
||||
val rsaPublicKey = KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(publicKeySpec)
|
||||
|
||||
val secretKeyBase64String = Base64.encodeToString(secretKeyBytes, Base64.NO_WRAP)
|
||||
val plaintextUtf8Bytes = secretKeyBase64String.toByteArray(StandardCharsets.UTF_8)
|
||||
|
||||
val rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION)
|
||||
rsaCipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey)
|
||||
val cipherTextBytes = rsaCipher.doFinal(plaintextUtf8Bytes)
|
||||
|
||||
return Base64.encodeToString(cipherTextBytes, Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ internal data class VisaConfig(
|
|||
val header: Header,
|
||||
@Json(name = "rsaPublicKey")
|
||||
val rsaPublicKey: RsaPublicKey,
|
||||
@Json(name = "rainRSAPublicKey")
|
||||
val rainRSAPublicKey: RsaPublicKey,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue