Updated on 2026-08-14
This commit is contained in:
parent
5579cc1372
commit
a04992591a
24 changed files with 707 additions and 29 deletions
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.data.pay.entity
|
||||
|
||||
internal data class EncryptedData(
|
||||
val encryptedBase64: String,
|
||||
val ivBase64: String,
|
||||
)
|
||||
|
|
@ -9,6 +9,8 @@ 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.datasource.api.pay.models.request.SetPinRequest
|
||||
import com.tangem.domain.pay.model.SetPinResult
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
|
|
@ -39,7 +41,7 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
override suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyHex, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
|
|
@ -51,14 +53,15 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
val pan = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.pan.secret,
|
||||
base64Iv = result.pan.iv,
|
||||
secretKeyHex = secretKeyHex,
|
||||
secretKeyBytes = secretKeyBytes,
|
||||
)
|
||||
|
||||
val cvv = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.cvv.secret,
|
||||
base64Iv = result.cvv.iv,
|
||||
secretKeyHex = secretKeyHex,
|
||||
secretKeyBytes = secretKeyBytes,
|
||||
)
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
TangemPayCardDetails(
|
||||
pan = pan,
|
||||
|
|
@ -69,6 +72,32 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setPin(pin: String): Either<UniversalError, SetPinResult> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes)
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
val status = requestHelper.request { authHeader ->
|
||||
tangemPayApi.setPin(
|
||||
authHeader = authHeader,
|
||||
body = SetPinRequest(
|
||||
sessionId = sessionId,
|
||||
pin = encryptedData.encryptedBase64,
|
||||
iv = encryptedData.ivBase64,
|
||||
),
|
||||
)
|
||||
}.result?.result ?: error("Cannot set pin code")
|
||||
when (status) {
|
||||
SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS
|
||||
SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK
|
||||
SetPinResult.DECRYPTION_ERROR.name -> SetPinResult.DECRYPTION_ERROR
|
||||
else -> SetPinResult.UNKNOWN_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPublicKeyBase64(): String {
|
||||
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
|
||||
return when (env) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
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.data.pay.entity.EncryptedData
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
|
|
@ -21,26 +20,38 @@ 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"
|
||||
private const val IV_LENGTH_BYTES = 16
|
||||
private const val PIN_LENGTH = 4
|
||||
private const val PIN_LENGTH_BYTES = 8
|
||||
private const val PIN_BLOCK_ISO_9564_FORMAT_PREFIX = '2'
|
||||
private const val PIN_BLOCK_FILL_CHAR = 'F'
|
||||
|
||||
internal class RainCryptoUtil @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend fun generateSecretKeyAndSessionId(publicKeyBase64: String): Pair<String, String> =
|
||||
suspend fun generateSecretKeyAndSessionId(publicKeyBase64: String): Pair<ByteArray, String> =
|
||||
withContext(dispatchers.default) {
|
||||
val secretKeyHex = ByteArray(KEY_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }.toHexString()
|
||||
val sessionId = generateSessionId(publicKeyBase64, secretKeyHex)
|
||||
secretKeyHex to sessionId
|
||||
val secretKeyBytes = ByteArray(KEY_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }
|
||||
val sessionId = generateSessionId(publicKeyBase64, secretKeyBytes)
|
||||
secretKeyBytes to sessionId
|
||||
}
|
||||
|
||||
suspend fun decryptSecret(base64Secret: String, base64Iv: String, secretKeyHex: String): String =
|
||||
suspend fun encryptPin(pin: String, secretKeyBytes: ByteArray): EncryptedData = withContext(dispatchers.default) {
|
||||
val bytes = pinBlockByteArray(pin)
|
||||
try {
|
||||
encryptSecret(bytes, secretKeyBytes)
|
||||
} finally {
|
||||
bytes.clear()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decryptSecret(base64Secret: String, base64Iv: String, secretKeyBytes: ByteArray): 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)
|
||||
|
||||
|
|
@ -51,8 +62,25 @@ internal class RainCryptoUtil @Inject constructor(
|
|||
plaintextBytes.toString(StandardCharsets.UTF_8).trim().ifEmpty { error("Invalid decrypted data") }
|
||||
}
|
||||
|
||||
private fun generateSessionId(publicKeyBase64: String, secretKeyHex: String): String {
|
||||
val secretKeyBytes = secretKeyHex.hexToBytes()
|
||||
private fun encryptSecret(bytes: ByteArray, secretKeyBytes: ByteArray): EncryptedData {
|
||||
val iv = ByteArray(IV_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }
|
||||
|
||||
val aesSecretKey = SecretKeySpec(secretKeyBytes, AES_ALGORITHM)
|
||||
val gcmSpec = GCMParameterSpec(TAG_LENGTH_BYTES * BITS_PER_BYTE, iv)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, aesSecretKey, gcmSpec)
|
||||
|
||||
val ciphertext = cipher.doFinal(bytes)
|
||||
bytes.clear()
|
||||
|
||||
return EncryptedData(
|
||||
encryptedBase64 = Base64.encodeToString(ciphertext, Base64.NO_WRAP),
|
||||
ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateSessionId(publicKeyBase64: String, secretKeyBytes: ByteArray): String {
|
||||
val publicKeyDerBytes = Base64.decode(publicKeyBase64, Base64.NO_WRAP)
|
||||
val publicKeySpec = X509EncodedKeySpec(publicKeyDerBytes)
|
||||
val rsaPublicKey = KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(publicKeySpec)
|
||||
|
|
@ -66,4 +94,26 @@ internal class RainCryptoUtil @Inject constructor(
|
|||
|
||||
return Base64.encodeToString(cipherTextBytes, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats PIN into a PIN block using schema: [Prefix][Length][PIN][fill with F].
|
||||
* Example: 246784FFFFFFFFFF for PIN 6784.
|
||||
*/
|
||||
private fun pinBlockByteArray(pin: String): ByteArray {
|
||||
require(pin.length == PIN_LENGTH) { "PIN length must be $PIN_LENGTH" }
|
||||
require(pin.all { it.isDigit() }) { "PIN must contain digits only" }
|
||||
|
||||
val pinBlockHexLength = PIN_LENGTH_BYTES * 2
|
||||
val hex = buildString(pinBlockHexLength) {
|
||||
append(PIN_BLOCK_ISO_9564_FORMAT_PREFIX)
|
||||
append(PIN_LENGTH.toString())
|
||||
append(pin)
|
||||
while (length < pinBlockHexLength) append(PIN_BLOCK_FILL_CHAR)
|
||||
}
|
||||
return hex.toByteArray(StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun ByteArray.clear() {
|
||||
for (i in indices) this[i] = 0
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue