Skip to content
This repository has been archived by the owner. It is now read-only.

Commit

Permalink
New cryptogram
Browse files Browse the repository at this point in the history
  • Loading branch information
a.ignatov committed Jul 3, 2023
1 parent c776762 commit 4609b6e
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 28 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ android {
versionName "1.0"
multiDexEnabled true
manifestPlaceholders = [
YANDEX_CLIENT_ID: ""
YANDEX_CLIENT_ID: "e7a80db136b0488fb420c8232531fa81"
]

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ class CloudpaymentsApi @Inject constructor(private val apiService: Cloudpayments
private const val THREE_DS_SUCCESS_URL = "https://api.cloudpayments.ru/threeds/success"
private const val THREE_DS_FAIL_URL = "https://api.cloudpayments.ru/threeds/fail"
}

fun getPublicKey(): Single<CloudpaymentsPublicKeyResponse> {
return apiService.getPublicKey()
.subscribeOn(Schedulers.io())
}

fun charge(requestBody: PaymentRequestBody): Single<CloudpaymentsTransactionResponse> {
return apiService.charge(requestBody)
.subscribeOn(Schedulers.io())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import ru.cloudpayments.sdk.api.models.CloudpaymentsBinInfoResponse
import ru.cloudpayments.sdk.api.models.CloudpaymentsPublicKeyResponse
import ru.cloudpayments.sdk.api.models.PaymentRequestBody
import ru.cloudpayments.sdk.api.models.ThreeDsRequestBody
import ru.cloudpayments.sdk.api.models.CloudpaymentsTransactionResponse
Expand All @@ -22,4 +23,7 @@ interface CloudpaymentsApiService {

@GET("bins/info/{firstSixDigits}")
fun getBinInfo(@Path("firstSixDigits") firstSixDigits: String): Single<CloudpaymentsBinInfoResponse>

@GET("payments/publickey")
fun getPublicKey(): Single<CloudpaymentsPublicKeyResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.cloudpayments.sdk.api.models

import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize

@Parcelize
data class CardCryptogramPacket(
@SerializedName("Type") var type: String? = "CloudCard",
@SerializedName("BrowserInfoBase64") var browserInfoBase64: String? = "",
@SerializedName("CardInfo") var cardInfo: CardInfo?,
@SerializedName("KeyVersion") var keyVersion: String?,
@SerializedName("Format") var format: Int? = 1,
@SerializedName("Value") var value: String?) : Parcelable

@Parcelize
data class CardInfo(
@SerializedName("FirstSixDigits") var firstSixDigits: String?,
@SerializedName("LastFourDigits") var lastFourDigits: String?,
@SerializedName("ExpDateYear") var expDateYear: String?,
@SerializedName("ExpDateMonth") var expDateMonth: String?) :Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ru.cloudpayments.sdk.api.models

import com.google.gson.annotations.SerializedName

data class CloudpaymentsPublicKeyResponse(
@SerializedName("Pem") val pem: String?,
@SerializedName("Version") val version: Int?)

96 changes: 75 additions & 21 deletions sdk/src/main/java/ru/cloudpayments/sdk/card/Card.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package ru.cloudpayments.sdk.card

import android.text.TextUtils
import android.util.Base64
import android.util.Log
import com.google.gson.GsonBuilder
import ru.cloudpayments.sdk.api.models.CardCryptogramPacket
import ru.cloudpayments.sdk.api.models.CardInfo
import ru.cloudpayments.sdk.util.HexPacketHelper
import java.io.UnsupportedEncodingException
import java.security.InvalidKeyException
import java.security.KeyFactory
Expand All @@ -18,10 +23,12 @@ import javax.crypto.NoSuchPaddingException
class Card {
companion object {

@Deprecated("Use API: https://api.cloudpayments.ru/payments/publickey")
private fun getKeyVersion(): String {
return "04"
}

@Deprecated("Use API: https://api.cloudpayments.ru/payments/publickey")
private fun getPublicKey(): String {
return "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArBZ1NNjvszen6BNWsgyDUJvDUZDtvR4jKNQtEwW1iW7hqJr0TdD8hgTxw3DfH+Hi/7ZjSNdH5EfChvgVW9wtTxrvUXCOyJndReq7qNMo94lHpoSIVW82dp4rcDB4kU+q+ekh5rj9Oj6EReCTuXr3foLLBVpH0/z1vtgcCfQzsLlGkSTwgLqASTUsuzfI8viVUbxE1a+600hN0uBh/CYKoMnCp/EhxV8g7eUmNsWjZyiUrV8AA/5DgZUCB+jqGQT/Dhc8e21tAkQ3qan/jQ5i/QYocA/4jW3WQAldMLj0PA36kINEbuDKq8qRh25v+k4qyjb7Xp4W2DywmNtG3Q20MQIDAQAB"
}
Expand Down Expand Up @@ -104,6 +111,68 @@ class Card {
return false
}

private fun prepareCardNumber(cardNumber: String): String {
return cardNumber.replace("\\s".toRegex(), "")
}

fun createHexPacketFromData(cardNumber: String, cardExp: String, cardCvv: String, publicId: String, publicKey: String, keyVersion: Int): String? {

var clearPublicKey = publicKey.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "")

val clearNumber = prepareCardNumber(cardNumber)

val cryptogram = createCardCryptogram(clearNumber, cardExp, cardCvv, publicId, clearPublicKey)

val cardInfo = CardInfo(
firstSixDigits = clearNumber.substring(0, 6),
lastFourDigits = clearNumber.substring(clearNumber.length - 4),
expDateMonth = cardExp.substring(0, 2),
expDateYear = cardExp.substring(cardExp.length - 2)
)


var cardCryptogramPacket = CardCryptogramPacket(
cardInfo = cardInfo,
keyVersion = HexPacketHelper.numberToEvenLengthString(keyVersion),
value = cryptogram
)

var gson = GsonBuilder().disableHtmlEscaping().create();
var cardCryptogramPacketString = gson.toJson(cardCryptogramPacket)

cardCryptogramPacketString = Base64.encodeToString(cardCryptogramPacketString.toByteArray(), Base64.NO_WRAP).trim()

return cardCryptogramPacketString
}
@Throws(UnsupportedEncodingException::class, NoSuchPaddingException::class, NoSuchAlgorithmException::class, BadPaddingException::class,
IllegalBlockSizeException::class, InvalidKeyException::class)
fun createCardCryptogram(number: String, cardExp: String, cardCvv: String, publicId: String, publicKey: String): String? {
val cardNumber = prepareCardNumber(number)
var exp = cardExp.replace("/", "")
if (cardNumber.length < 14 || exp.length != 4) {
return null
}

exp = exp.substring(2, 4) + cardExp.substring(0, 2)
val s = "$cardNumber@$exp@$cardCvv@$publicId"
val bytes = s.toByteArray(charset("ASCII"))
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
val random = SecureRandom()

Log.e("KEY", "KEY: " + publicKey)

cipher.init(Cipher.ENCRYPT_MODE, getRSAKey(publicKey), random)
val crypto = cipher.doFinal(bytes)
var crypto64 = Base64.encodeToString(crypto, Base64.DEFAULT)
val cr_array = crypto64.split("\n").toTypedArray()
crypto64 = ""
for (i in cr_array.indices) {
crypto64 += cr_array[i]
}
return crypto64
}

@Deprecated("Use API: https://api.cloudpayments.ru/payments/publickey to get publicKey and keyVersion, then use new method createHexPacketFromData(number, cardExp, cardCvv, publicId, publicKey, keyVersion)")
@Throws(UnsupportedEncodingException::class, NoSuchPaddingException::class, NoSuchAlgorithmException::class, BadPaddingException::class,
IllegalBlockSizeException::class, InvalidKeyException::class)
fun cardCryptogram(number: String, cardExp: String, cardCvv: String, publicId: String): String? {
Expand All @@ -120,11 +189,12 @@ class Card {
val bytes = s.toByteArray(charset("ASCII"))
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
val random = SecureRandom()
cipher.init(Cipher.ENCRYPT_MODE, getRSAKey(), random)
cipher.init(Cipher.ENCRYPT_MODE, getRSAKey(getPublicKey()), random)
val crypto = cipher.doFinal(bytes)
var crypto64 = "01" +
shortNumber +
exp + getKeyVersion() +
exp +
getKeyVersion() +
Base64.encodeToString(crypto, Base64.DEFAULT)
val cr_array = crypto64.split("\n").toTypedArray()
crypto64 = ""
Expand All @@ -134,24 +204,12 @@ class Card {
return crypto64
}

/**
* Генерим криптограму для CVV
* @param cardCvv
* @return
* @throws UnsupportedEncodingException
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws InvalidKeyException
*/

@Throws(UnsupportedEncodingException::class, NoSuchPaddingException::class, NoSuchAlgorithmException::class, BadPaddingException::class, IllegalBlockSizeException::class, InvalidKeyException::class)
fun cardCryptogramForCVV(cardCvv: String): String? {
val bytes = cardCvv.toByteArray(charset("ASCII"))
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
val random = SecureRandom()
cipher.init(Cipher.ENCRYPT_MODE, getRSAKey(), random)
cipher.init(Cipher.ENCRYPT_MODE, getRSAKey(getPublicKey()), random)
val crypto = cipher.doFinal(bytes)
var crypto64 = "03" + getKeyVersion() + Base64.encodeToString(crypto, Base64.DEFAULT)
val crArray = crypto64.split("\n").toTypedArray()
Expand All @@ -162,14 +220,10 @@ class Card {
return crypto64
}

private fun prepareCardNumber(cardNumber: String): String {
return cardNumber.replace("\\s".toRegex(), "")
}

private fun getRSAKey(): PublicKey? {
private fun getRSAKey(publicKey: String): PublicKey? {
return try {
val keyBytes: ByteArray =
Base64.decode(getPublicKey().toByteArray(charset("utf-8")), Base64.DEFAULT)
Base64.decode(publicKey.toByteArray(charset("utf-8")), Base64.DEFAULT)
val spec = X509EncodedKeySpec(keyBytes)
val kf: KeyFactory = KeyFactory.getInstance("RSA")
kf.generatePublic(spec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ru.cloudpayments.sdk.databinding.DialogCpsdkPaymentCardBinding
import ru.cloudpayments.sdk.models.Currency
import ru.cloudpayments.sdk.scanner.CardData
import ru.cloudpayments.sdk.ui.dialogs.base.BasePaymentBottomSheetFragment
import ru.cloudpayments.sdk.util.PublicKey
import ru.cloudpayments.sdk.util.TextWatcherAdapter
import ru.cloudpayments.sdk.util.hideKeyboard
import ru.cloudpayments.sdk.viewmodel.PaymentCardViewModel
Expand Down Expand Up @@ -158,11 +159,13 @@ internal class PaymentCardFragment :
val cardExp = binding.editCardExp.text.toString()
val cardCvv = binding.editCardCvv.text.toString()

val cryptogram = Card.cardCryptogram(
val cryptogram = Card.createHexPacketFromData(
cardNumber,
cardExp,
cardCvv,
paymentConfiguration?.publicId ?: ""
paymentConfiguration?.publicId ?: "",
PublicKey.getInstance(requireContext()).pem ?: "",
PublicKey.getInstance(requireContext()).version ?: 0
)

if (isValid() && cryptogram != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ru.cloudpayments.sdk.ui.dialogs

import android.os.Bundle
import android.text.Editable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand All @@ -21,6 +22,7 @@ import com.yandex.pay.core.data.PaymentMethodType
import ru.cloudpayments.sdk.databinding.DialogCpsdkPaymentOptionsBinding
import ru.cloudpayments.sdk.ui.PaymentActivity
import ru.cloudpayments.sdk.ui.dialogs.base.BasePaymentBottomSheetFragment
import ru.cloudpayments.sdk.util.PublicKey
import ru.cloudpayments.sdk.util.TextWatcherAdapter
import ru.cloudpayments.sdk.util.emailIsValid
import ru.cloudpayments.sdk.util.hideKeyboard
Expand Down Expand Up @@ -73,6 +75,13 @@ internal class PaymentOptionsFragment :
} else {
binding.buttonYandexpay.visibility = View.GONE
}

if (state.publicKeyPem != null && state.publicKeyVersion != null ) {
context?.let {
PublicKey.getInstance(it).savePem(state.publicKeyPem)
PublicKey.getInstance(it).saveVersion(state.publicKeyVersion)
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -165,6 +174,8 @@ internal class PaymentOptionsFragment :
listener?.onGooglePayClicked()
dismiss()
}

viewModel.getPublicKey()
}

private fun updateStateButtons() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ internal abstract class BaseVMDialogFragment<VS : BaseViewState, VM : BaseViewMo
viewModel.viewState.observe(viewLifecycleOwner, Observer {
render(it)
})

}

override fun onStart() {
Expand Down
13 changes: 13 additions & 0 deletions sdk/src/main/java/ru/cloudpayments/sdk/util/HexPacketHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.cloudpayments.sdk.util

class HexPacketHelper {

companion object {
fun numberToEvenLengthString(number: Int): String {
var numberStr = number.toString()

return if (numberStr.length % 2 == 0) numberStr
else "0$numberStr";
}
}
}
50 changes: 50 additions & 0 deletions sdk/src/main/java/ru/cloudpayments/sdk/util/PublicKey.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ru.cloudpayments.sdk.util

import android.app.Activity
import android.content.Context
import android.content.SharedPreferences

class PublicKey private constructor() {

companion object {
private val publicKey = PublicKey()
private lateinit var sharedPreferences: SharedPreferences

private const val PEM = "pem"
private const val VERSION = "version"

fun getInstance(context: Context): PublicKey {
if (!::sharedPreferences.isInitialized) {
synchronized(PublicKey::class.java) {
if (!::sharedPreferences.isInitialized) {
sharedPreferences = context.getSharedPreferences(context.packageName, Activity.MODE_PRIVATE)
}
}
}
return publicKey
}
}

val pem: String?
get() = sharedPreferences.getString(PEM, "")

fun savePem(pem: String) {
sharedPreferences.edit()
.putString(PEM, pem)
.apply()
}

val version: Int?
get() = sharedPreferences.getInt(VERSION, 0)

fun saveVersion(version: Int) {
sharedPreferences.edit()
.putInt(VERSION, version)
.apply()
}

fun clearAll() {
sharedPreferences.edit().clear().apply()
}

}
Loading

0 comments on commit 4609b6e

Please sign in to comment.