CPoC SDK
Getting Started
Quickstart Guide

CPoC SDK Quickstart

🧑🏽‍💻

If you want to code along, you can checkout the example project here via github (opens in a new tab)

A reference to Extension & Util might be helpful when you're experimenting with the SDK.

Init SDK

To init the SDK, first get the MhdCPOC instance.

ExampleViewModel.kt
private val sdk = MhdCPOC.getInstance(app)

Then you can invoke MhdCPoC_Init2, passing along with the .license filename. During SDK initialization, it'll perform:

  • local runtime checking, like secure key store, root, debugger attached and developer mode detection
  • network request with the MineSec Attestation & Monitoring Service (AMS), checking includes SafetyNet, Play Integrity, or/ and key attestation. These online checking rules can be configured in the backend.

An valid online attestation result could last for 300 minutes to mitigate poor network condition.

ExampleViewModel.kt
init {
    initSdk()
}
 
private fun initSdk() = viewModelScope.launch(Dispatchers.IO) {
    writeMessage("Start init SDK")
    val yourDeviceId = "client-example-123"
    val licenseFileName = "example.license"
    val initResult = sdk.MhdCPOC_Init2(app, licenseFileName, yourDeviceId)
    writeMessage("init result: \n${prettyGson.toJson(initResult)}")
}
💡

As the SDK initialization involve network request, on Android you'll need to run it in background thread (IO). Run the init call in main thread (UI) would crash the app. You can run the job with the coroutine Dispatcher.IO.

You can also retrieve the minesec sdkId by getting mineHadesIdentifier, the sdk id is:

  • generated by minesec
  • unique per
    1. application package name;
    2. device (hardware id)

We recommend you to store SDK ID in your system as well for tracking issue/ debugging.

ExampleViewModel.kt
fun getSdkInfo() {
    writeMessage("SDK version: ${sdk.mineHadesVersion}")
    writeMessage("SDK ID (By MineSec): ${sdk.mineHadesIdentifier}")
}
Output
SDK Version: 1.10.105.12
SDK ID (By MineSec): 1234567890abcdef

Configure EMV parameters

Once the SDK is registered and initialized, you can start configure the EMV parameters. There are 3 types of parameter:

  • EMV kernel app param
  • Scheme CA public keys (CAPKs)
  • Terminal param

EMV Kernel App Param

The EMV app param refers to the kernel configuration, you can prepare it in your payment backend and pass it to the SDK. A reference can be found in the example project's asset directory.

      • emv-app.json
  • Pass the data object to SDK with MhdEmv_AddApp():

    💡

    (Optional) Dispatch the job with Dispatcher.Default to avoid blocking the UI

    ExampleViewModel.kt
    fun setEmvApps() = viewModelScope.launch(Dispatchers.Default) {
        writeMessage("setEmvApps")
        val emvApps: List<EmvApp> = app.loadJsonFromAsset("emv-app.json")
        emvApps.forEach { emvApp ->
            val result = sdk.MhdEmv_AddApp(emvApp.toMhdEmvApp())
            writeMessage("EMV AID ${emvApp.aid}\nsuccess?: ${result == MhdReturnCode.MHD_SUCCESS}")
        }
    }
    The SDK can hold up to 32 EMV apps

    You can get the existing EmvAppParam from the SDK:

    ExampleViewModel.kt
    private val maxEmvAppSlots = 32
    fun getEmvApps() = viewModelScope.launch {
        writeMessage("getEmvApps")
        for (i in 0 until maxEmvAppSlots) {
            val emvAppDump = EMV_APPLIST()
            sdk.MhdEmv_GetApp(i, emvAppDump)
            if (emvAppDump.aid.isAllZero()) break
     
            writeMessage("EmvApp $i, AID: ${emvAppDump.toEmvApp().aid}\n${emvAppDump.toEmvApp()}")
        }
    }

    CAPK Param

    Like the EMV app params, you could refer to the default value in capk.json. The CAPKs should be rather static in most cases unless the card schemes update it.

    ExampleViewModel.kt
    fun setCapks() = viewModelScope.launch(Dispatchers.Default) {
        Log.d(TAG, "setCapks")
        val capks: List<Capk> = app.loadJsonFromAsset("capk.json")
        capks.forEach { capk ->
            val result = sdk.MhdEmv_AddCapk(capk.toMhdCapk())
            Log.d(TAG, "CAPK RID ${capk.rid}\nsuccess?: ${result == MhdReturnCode.MHD_SUCCESS}")
        }
    }
    Note that the SDK can hold up to 64 CAPKs

    To read existing CAPKs from SDK:

    ExampleViewModel.kt
    private val maxCapkSlots = 64
    fun getCapks() {
        Log.d(TAG, "getCapks")
        for (i in 0 until maxCapkSlots) {
            val msCapkDump = EMVCAPK()
            sdk.MhdEmv_GetCapk(i, msCapkDump)
            if (msCapkDump.rid.isAllZero()) break
     
            Log.d(TAG, "Capk $i, RID: ${msCapkDump.toCapk().rid}\n${msCapkDump.toCapk()}")
        }
    }

    Terminal (Device) Param

    Lastly, you can set the TerminalParams via MhdEmv_SetParam(). The terminal params is shared across different EMV apps, e.g. the terminalCapability, transaction currency code & etc

    💡

    (Optional) Dispatch the job with Dispatcher.Default to avoid blocking the UI

    ExampleViewModel.kt
    fun setTermParam() = viewModelScope.launch(Dispatchers.Default) {
        writeMessage("setTermParam")
     
        // termCap: 0060c8
        // × plaintext offline pin
        // ✓ enciphered online pin
        // ✓ signature
        // × enciphered offline pin
        // × no cvm
        val termParams: TerminalParam = app.loadJsonFromAsset("term.json")
        val result = sdk.MhdEmv_SetParam(termParams.toMhdTermParams(), app)
        writeMessage("TermParams success?: ${result == MhdReturnCode.MHD_SUCCESS}")
    }

    To get the existing TerminalParams from the SDK:

    ExampleViewModel.kt
    fun getTermParam() = viewModelScope.launch {
        writeMessage("getTermParam")
     
        val dump = EMV_PARAM()
        sdk.MhdEmv_GetParam(dump)
        writeMessage("getTermParam: ${dump.toTermParams()}")
    }

    Key Ceremony

    The basic idea

    First let's prepare the initial key for the device. You can find the DukptAesHost util in the example project.

    ⚠️

    Note that below are just sample/ demonstration code. DO NOT generate/ wrap keys locally in any production application

    First let's create a demo BDK:

    // !!!!!!! DEMO ONLY !!!!!!!
    // AES 128 BDK
    // DO NOT do it in any production environment
    // always use HSM or equivalent secure module
    private val dangerouslyLocalCardBdk = "f1".repeat(16)
    private val dangerouslyLocalPinBdk = "f2".repeat(16)

    For the key wrapping, the SDK support 3 wrapping methods:

    • Simple RSA
    • RSA OAEP
    • TR31

    Let's take a look of the wrapping process, note that this should only happen in secure backend system, the below code is just a demo:

    Once we have the wrapped key, you can pass it to the SDK with CryptoInjectKey().

    Backend wrapping key (RSA Simple & RSA OAEP)

    In real world scenario, you should get the RSA public for key wrapping from your backend side, refer to the AMS Quickstart

    enum class WrappingMethod(val minesecInt: Int) {
        RSA(MsKeyProperties.WRAPPING_METHOD_RSA),
        RSA_OAEP_SHA256(MsKeyProperties.WRAPPING_METHOD_RSA_OAEP_SHA256)
    }
     
    enum class ClientKeyType(val keyAlias: String, val msKeyProp: String) {
        CARD_KEY("client_card_key", MsKeyProperties.MINESEC_CARD_KEY_NAME),
        PIN_KEY("client_pin_key", MsKeyProperties.MINESEC_PIN_KEY_NAME),
    }
     
    // Simulate initial key wrapping
    // !!!!!!! DEMO ONLY !!!!!!!
    // DO NOT do it in any production environment
    private fun dangerouslyLocalWrapIkWithMineSecPublic(
        bdk: ByteArray,
        keyAlias: String,
        wrappingMethod: WrappingMethod = WrappingMethod.RSA_OAEP_SHA256,
    ): MsWrappedSecretKeyEntry? {
        val mineSecPubName = "minesecpk"
     
        val dangerouslyLocalIkId = 16.sequentialString()
        val dangerouslyLocalIk = DukptAesHost.deriveInitialKeyByBdk(
            bdk,
            KeyType.AES128,
            dangerouslyLocalIkId
        )
     
        // support 3 modes;
        // - Simple RSA
        // - RSA OAEP
        // - TR31, check doc for more details
        val publicStr = KeyLoader.ReadKeyfromKeyStore(mineSecPubName).wrappedKey.decodeToString()
        val kekPublic = RSAUtils.getPublicKeyFromPEM(publicStr)
     
        val dangerouslyLocalWrapEntry = when (wrappingMethod) {
            WrappingMethod.RSA -> RSAUtils.simpleCrypt(
                mode = Cipher.ENCRYPT_MODE,
                key = kekPublic,
                data = dangerouslyLocalIk
            )
     
            WrappingMethod.RSA_OAEP_SHA256 -> RSAUtils.oaepMgf1Sha256Crypt(
                mode = Cipher.ENCRYPT_MODE,
                key = kekPublic,
                data = dangerouslyLocalIk
            )
        }
     
        return MsWrappedSecretKeyEntry.Builder(
            keyAlias,
            dangerouslyLocalWrapEntry,
            MsKeyProperties.KEY_TYPE_AES_AES128
        )
            .setKeyId(dangerouslyLocalIkId)
            .setWrappingKeyAlias(MsKeyProperties.MINESEC_KEK_NAME)
            .setWrappingMethod(wrappingMethod.minesecInt)
            .setKeyUsage(MsKeyProperties.KEY_USAGE_DUKPT_INITIAL_KEY)
            .build()
    }

    Inject Wrapped Initial Key(s) to SDK

    After the application gets the wrapped key, you can inject it to the SDK like:

    ExampleViewModel.kt
    /**
     * MineHades full SDK supports you to load a DUKPT Initial Key(AES-128) in the app initialization stage.
     * Once the IK is injected into SDK. you can derive working key during each transaction.
     * DUKPT Initial Key shall be encrypted using the minesec public key in **server side**,
     * and download it into SDK via key loading interface.
     */
    fun injectCardInitialKey() = viewModelScope.launch(Dispatchers.Default) {
        // should be getting from your backend
        val wrappedKeyEntryCard = dangerouslyLocalWrapIkWithMineSecPublic(
            dangerouslyLocalCardBdk.hexToByteArray(),
            ClientKeyType.CARD_KEY.keyAlias,
            WrappingMethod.RSA
        )
        val injectResp = sdk.payInterface.CryptoInjectKey(wrappedKeyEntryCard)
        writeMessage("Card key injectResp, code: ${injectResp.errorcode}, msg: ${injectResp.errorMsg}")
    }
     
    fun injectPinInitialKey() = viewModelScope.launch(Dispatchers.Default) {
        // should be getting from your backend
        val wrappedKeyEntryPin = dangerouslyLocalWrapIkWithMineSecPublic(
            dangerouslyLocalPinBdk.hexToByteArray(),
            ClientKeyType.PIN_KEY.keyAlias,
        )
        val injectResp = sdk.payInterface.CryptoInjectKey(wrappedKeyEntryPin)
        writeMessage("PIN key injectResp, code: ${injectResp.errorcode}, msg: ${injectResp.errorMsg}")
    }

    Note that in this demo, we'll be using client_card_key & client_pin_key as the card encryption & pin key respectively (defined in the enum ClientKeyType)

    Contactless Card Read

    We'll switch to the UI layer (Activity/ Compose) instead of viewModel from now on as we will be dealing with foreground and NFC

    Request Camera Permission

    To process card read, the first thing to make sure is the application is granted with CAMERA permission.

    You can request the permission any way you wish. Here's just a demo handling for your reference:

    In the demo code, we're using the Compose & Jetpack Compose Permissions (opens in a new tab) from accompanist compose lib:

    ExampleSoftPosDemoSection.kt
    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun AndroidPermission(content: @Composable () -> Unit) {
        // camera permission
        var didRequestPermission by rememberSaveable { mutableStateOf(false) }
        val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) {
            didRequestPermission = true
        }
        val localContext = LocalContext.current
     
        when {
            // happy path
            cameraPermissionState.status.isGranted -> content()
     
            // permanently denied
            didRequestPermission && !cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale -> {
                // permanently denied
                Text(text = "Camera permission permanently denied")
                Text(text = "Please open the app setting and grant permission")
                BrandedButton(
                    label = "Open up app setting",
                    onClick = {
                        // if permission is permanently denied, redirect to app setting
                        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                            .apply {
                                data = Uri.fromParts("package", localContext.packageName, null)
                            }
                        localContext.startActivity(intent)
                    }
                )
            }
     
            // finally show request permission button
            else -> {
                Text(text = "First we'll need the CAMERA permission")
                BrandedButton(
                    label = "Request Camera Permission",
                    onClick = {
                        cameraPermissionState.launchPermissionRequest()
                    })
            }
        }
    }

    Toggle NFC

    The next step is to enable the NFC interface, the method is rather straight forward.

    Note that for the enable method it requires the Activity.

    // to enable NFC
    sdk.MhdNfc_Enablev2(activity)
     
    // to disable NFC
    sdk.MhdNfc_DisableReader()

    Let's take a look of how this would be in a Composable:

    UI (Composable)
    // Inside of the composable
     
    // hold a reference of the sdk
    val sdk = remember { MhdCPOC.getInstance(localContext) }
    // local ui state for the toggle NFC enable
    var uiReaderEnabled by remember { mutableStateOf(false) }
     
    LaunchedEffect(uiReaderEnabled) {
        if (uiReaderEnabled) {
            lifecycleOwner.lifecycleScope.launch(Dispatchers.Default) {
                val nfcEnableResult = sdk.MhdNfc_Enablev2(localContext.findActivity())
                viewModel.writeMessage("NFC enable? ${nfcEnableResult.errorMsg}")
            }
        } else {
            sdk.MhdNfc_DisableReader()
            Log.d(TAG, "NFC disabled")
        }
    }

    Register EMV callback

    Permission requested, NFC enabled, we can now start making EMV transaction request like below:

    sdk.payInterface.EmvPerformTransaction(localContext, txnRequest)
    ⚠️

    Aware that when the transaction requires to perform PIN entry, the SDK would prompt a secure PIN Pad, and you'll need to call the EmvPerformTransaction inside a thread

    Also in the above demo code, we've injected 2 initial keys (card & pin) with our own KeyAlias, so we'll need to call SDK to derive working keys before transaction. If we didn't invoke the derive function, the SDK would use the default key for working key derivation.

    UI (Composable)
    if (uiReaderEnabled) {
        // ...enable NFC
     
        // callback for card read data
        sdk.setOnEmvHandler {
            Log.d(TAG, "onEmvHandler")
            thread {
                // since we've injected our own IK for card & PIN,
                // before transaction we'll need to tell SDK to derive working key (for the correct KSN)
                viewModel.deriveWorkingKeysBeforeTran()
     
                val txnRequest = MhdEmvTransactionDto
                    .builder()
                    // 2.00
                    .txnAmount(200)
                    .txnType(MhdEmvTransactionType.MHD_EMV_TRANS_PURCHASE)
                    // auto prompt PIN pad when PIN CVM is applicable
                    // default is enabled
                    //.autoPinEntry(true)
     
                    // since we'll using own IK and call derive manually
                    // turn off the `deriveKeysFromIK` to avoid sdk default IK derivation
                    .deriveKeysFromIK(false)
     
                    // will handle the EPB by the real PAN
                    .enablePANToken(false)
                    .build()
     
                val mineHadesResult = sdk.payInterface.EmvPerformTransaction(localContext, txnRequest)
                val cardReadResult = mineHadesResult.toCardReadResult()
                viewModel.writeMessage("cardReadResult: \n${prettyGson.toJson(cardReadResult)}")
            }
        }
    }

    Back to the viewModel here's the SDK derive part:

    ExampleSoftPosDemoSection.kt
    fun deriveWorkingKeysBeforeTran() {
        writeMessage("since we've injected our own IK for card & PIN, before transaction we'll need to tell SDK to derive working key (for the correct KSN)")
     
        val keyGenCardWk = DukptKeyGenParameter
            .Builder(ClientKeyType.CARD_KEY.msKeyProp, ClientKeyType.CARD_KEY.keyAlias)
            .setKeyUsage(MsKeyProperties.KEY_USAGE_DATA_ENC_BOTH)
            .setIsCounterUpdate(true)
            .setKeyType(MsKeyProperties.KEY_TYPE_AES_AES128)
            .build()
        val cardWkRes = sdk.MhdSdk_GetKeyStore().DeriveKey(keyGenCardWk)
        writeMessage("next card ksn: ${cardWkRes.data.ksn}")
     
        val keyGenPinWk = DukptKeyGenParameter
            .Builder(ClientKeyType.PIN_KEY.msKeyProp, ClientKeyType.PIN_KEY.keyAlias)
            .setKeyUsage(MsKeyProperties.KEY_USAGE_PIN_ENCRYPTION)
            .setIsCounterUpdate(true)
            .setKeyType(MsKeyProperties.KEY_TYPE_AES_AES128)
            .build()
        val pinWkRes = sdk.MhdSdk_GetKeyStore().DeriveKey(keyGenPinWk)
        writeMessage("next pin ksn: ${pinWkRes.data.ksn}")
    }

    Alright, you can try tap your card to the mobile phone and see the result.

    The sensitive data (like track 2 data tag57 & pin block tag99) returned from the SDK is automatically encrypted. Something like below

    tag57: 44c7c9b4674408a4e97eebbfda2f2b5c2c217ba136c908cc5fc243b67d3e04f50ac50b0f4e627cc81fe60b2fe3cc49b7
    tag99: 142b2aa274a4d7206f995a1ee215ded2

    The below part is a demo the process of handling the data locally. In reality you can just pass all the data to your backend system and construct the payment message like ISO8583 or json.

    In order to further process to the payment host, we'll also need the KSN, initial vector (IV), which you can obtain from the returned transaction data.

    Let's create some value class (opens in a new tab) for the clarity & better readability,

    @JvmInline
    value class Iv(val value: String)
     
    @JvmInline
    value class Ksn(val value: String)
     
    @JvmInline
    value class Encrypted57(val value: String)
     
    @JvmInline
    value class EncryptedPinBlock(val value: String)
     
    @JvmInline
    value class PanToken(val value: String)

    To get the EMV tags, you can access it from the result.emvData["emvTag"].

    The result.emvData["57"] is formatted in {iv}{encryptedData}

    • iv: initialization vector, 32 length hex
    • encryptedData: the remaining length of the string be the encrypted data

    So back to the UI, we'll save the necessary data in view model for later decryption

    viewModel.setCardReadResult(Triple(cardKsn, iv, encrypted57))
    viewModel.setEncryptedPinData(Triple(pinKsn, epbHex, null))

    In full:

    UI Composable
    sdk.setOnEmvHandler {
        thread {
            //... init the transaction
     
            // after the result
            if (cardReadResult is CardReadResult.Success) {
                // holding encrypted card data in viewmodel for later demo decrypt
                println(cardReadResult.emvData["57"])
                cardReadResult.emvData["57"]?.let {
                    val cardKsn = Ksn(cardReadResult.cardKsn)
                    val iv = Iv(it.substring(0, 32))
                    val encrypted57 = Encrypted57(it.substring(32))
                    viewModel.setCardReadResult(Triple(cardKsn, iv, encrypted57))
                    viewModel.writeMessage("required for decryption (in backend): $cardKsn, $iv, $encrypted57")
                }
     
                // holding encrypted pin data in viewmodel for later demo decrypt
                println(cardReadResult.emvData["99"])
                cardReadResult.emvData["99"]?.let {
                    val pinKsn = Ksn(cardReadResult.pinKsn!!)
                    val epbHex = EncryptedPinBlock(it)
                    viewModel.setEncryptedPinData(Triple(pinKsn, epbHex, null))
                    viewModel.writeMessage("required for translate: $pinKsn, $epbHex")
                } ?: run {
                    viewModel.setEncryptedPinData(null)
                }
            } else {
                viewModel.writeMessage("cardReadResult failed: \n${prettyGson.toJson(cardReadResult)}")
                viewModel.setCardReadResult(null)
            }
        }
    }

    Decrypt Data

    After reading the card data and the PIN data, you can send it alongside with the KSNs to the backend processing system for constructing the payment message (e.g. ISO8583 or json).

    Here are some example showing the flow & gist below:

    Encrypted Card Data

    // DEMO PURPOSE ONLY
    // Data decrypt
    fun dangerouslyDecryptCardDataLocally() {
        writeMessage("card read result: ${cardReadResult.value.toString()}")
        cardReadResult.value?.let { (ksn, iv, encrypted) ->
            // get working key by ksn and bdk
            val dangerouslyDemoWorkingCardKey = DukptAesHost.deriveWorkingKeyByBdk(
                bdk = dangerouslyLocalCardBdk.hexToByteArray(),
                bdkKeyType = KeyType.AES128,
                workingKeyType = KeyType.AES128,
                workingKeyUsage = KeyUsage.DataEncryptionBothWays,
                ksn = ksn.value
            )
            try {
                val plainTrack2 = Aes.decrypt(
                    encrypted.value.hexToByteArray(),
                    dangerouslyDemoWorkingCardKey,
                    Aes.Padding.PKCS5Padding,
                    iv.value.hexToByteArray()
                )
                val plainPan = plainTrack2.toHexString().lowercase().substringBefore("d")
                writeMessage("decrypted track 2: ${plainTrack2.toHexString()}")
                writeMessage("decrypted PAN: $plainPan")
     
                // set PanToken for the EPB translate
                viewModelScope.launch {
                    _encryptedPinData.emit(_encryptedPinData.value?.copy(third = PanToken(plainPan)))
                }
            } catch (e: Exception) {
                writeMessage("err: $e")
            }
        }
    }

    Encrypted PIN Block

    The encrypted PIN block (EPB) returned from the SDK is in ISO9564 PIN Block format 4, hence a PAN is required. Note the the above dangerouslyDecryptCardDataLocally also store the plain PAN

    viewModelScope.launch {
        _encryptedPinData.emit(_encryptedPinData.value?.copy(third = PanToken(plainPan)))
    }

    Like the data encryption, we'll need the pinKsn as well.

    fun dangerouslyDecryptPinDataLocally() {
        writeMessage("epb: ${encryptedPinData.value.toString()}")
        encryptedPinData.value?.let { (ksn, epb, panToken) ->
            // get working key by ksn and bdk
            val dangerouslyDemoWorkingPinKey = DukptAesHost.deriveWorkingKeyByBdk(
                bdk = dangerouslyLocalPinBdk.hexToByteArray(),
                bdkKeyType = KeyType.AES128,
                workingKeyType = KeyType.AES128,
                workingKeyUsage = KeyUsage.PinEncryption,
                ksn = ksn.value
            )
     
            try {
                writeMessage("$ksn, $epb, $panToken")
                panToken?.let {
                    val plainPin = PinBlock.dangerouslyDecryptIso4EpbToPin(
                        dangerouslyDemoWorkingPinKey,
                        epb.value.hexToByteArray(),
                        encryptedPinData.value?.third?.value!!
                    )
                    writeMessage("decrypted PIN: $plainPin")
                } ?: writeMessage("no pan token, please decrypt first")
            } catch (e: Exception) {
                writeMessage("err: $e")
            }
        }
    }

    Great! You have successfully init the SDK, setting all the configs, perform the key injection, tapping card and decrypt it!

    The next step you might want to check

    • Publish your app on the Play Store
    • Request the production license/ environment setups from MineSec
    • L3 testing with your acquiring bank

    Feel free to drop us a hi for further help & discussion!