CPoC SDK Quickstart
If you want to code along, you can checkout the example project here via github (opens in a new tab)
- Starting branch (opens in a new tab): Already setup with the basic UIs
- Final branch (opens in a new tab): Completed for Init, configure params, request card read & PIN pad, and an example how to handle the encrypted data in the host side
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.
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.
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
- application package name;
- device (hardware id)
We recommend you to store SDK ID in your system as well for tracking issue/ debugging.
fun getSdkInfo() {
writeMessage("SDK version: ${sdk.mineHadesVersion}")
writeMessage("SDK ID (By MineSec): ${sdk.mineHadesIdentifier}")
}
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
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}")
}
}
You can get the existing EmvAppParam from the SDK:
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.
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}")
}
}
To read existing CAPKs from SDK:
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
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:
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:
/**
* 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:
@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:
// 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.
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:
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 hexencryptedData
: 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:
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!