Reputation: 1
Hi everyone I'm creating an application that will mimic the emv card and return the data to POS Terminal issue After recieving the PPSE command I'm unable to receive second incoming command and my service deatcivate with reason 1
My Manifiest File
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.qifan.nfcbank">
<uses-permission android:name="android.permission.NFC"/>
<uses-feature android:name="android.hardware.nfc.hce"
android:required="true"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity android:name=".CardEmulatorActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- <activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
<activity android:name=".NFCSendActivity">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/com.qifan.nfcbank"/>
</intent-filter>
</activity>
-->
<!--<service
android:name=".cardEmulation.MyHostApduService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE" >
<intent-filter>
<action android:name="android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE"/>
</intent-filter>
<meta-data android:name="android.nfc.cardemulation.off_host_apdu_service"
android:resource="@xml/apduservice"/>
</service>-->
<service android:name=".cardEmulation.KHostApduService"
android:exported="true"
android:enabled="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<!-- Intent filter indicating that we support card emulation. -->
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<!-- Required XML configuration file, listing the AIDs that we are emulating cards
for. This defines what protocols our card emulation service supports. -->
<meta-data android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice"/>
</service>
</application>
</manifest>
My Service Conde
package com.qifan.nfcbank.cardEmulation
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.cardemulation.HostApduService
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.core.app.NotificationCompat
import com.qifan.nfcbank.R
import java.io.UnsupportedEncodingException
import java.math.BigInteger
import java.util.*
/**
* Created by Qifan on 05/12/2018.
*/
class KHostApduService : HostApduService() {
private val TAG = "HostApduService"
private val APDU_SELECT = byteArrayOf(
0x00.toByte(), // CLA - Class - Class of instruction
0xA4.toByte(), // INS - Instruction - Instruction code
0x04.toByte(), // P1 - Parameter 1 - Instruction parameter 1
0x00.toByte(), // P2 - Parameter 2 - Instruction parameter 2
0x07.toByte(), // Lc field - Number of bytes present in the data field of the command
0xD2.toByte(),
0x76.toByte(),
0x00.toByte(),
0x00.toByte(),
0x85.toByte(),
0x01.toByte(),
0x01.toByte(), // NDEF Tag Application name
0x00.toByte(), // Le field - Maximum number of bytes expected in the data field of the response to the command
)
private val CAPABILITY_CONTAINER_OK = byteArrayOf(
0x00.toByte(), // CLA - Class - Class of instruction
0xa4.toByte(), // INS - Instruction - Instruction code
0x00.toByte(), // P1 - Parameter 1 - Instruction parameter 1
0x0c.toByte(), // P2 - Parameter 2 - Instruction parameter 2
0x02.toByte(), // Lc field - Number of bytes present in the data field of the command
0xe1.toByte(),
0x03.toByte(), // file identifier of the CC file
)
private val READ_CAPABILITY_CONTAINER = byteArrayOf(
0x00.toByte(), // CLA - Class - Class of instruction
0xb0.toByte(), // INS - Instruction - Instruction code
0x00.toByte(), // P1 - Parameter 1 - Instruction parameter 1
0x00.toByte(), // P2 - Parameter 2 - Instruction parameter 2
0x0f.toByte(), // Lc field - Number of bytes present in the data field of the command
)
// In the scenario that we have done a CC read, the same byte[] match
// for ReadBinary would trigger and we don't want that in succession
private var READ_CAPABILITY_CONTAINER_CHECK = false
private val READ_CAPABILITY_CONTAINER_RESPONSE = byteArrayOf(
0x00.toByte(), 0x11.toByte(), // CCLEN length of the CC file
0x20.toByte(), // Mapping Version 2.0
0xFF.toByte(), 0xFF.toByte(), // MLe maximum
0xFF.toByte(), 0xFF.toByte(), // MLc maximum
0x04.toByte(), // T field of the NDEF File Control TLV
0x06.toByte(), // L field of the NDEF File Control TLV
0xE1.toByte(), 0x04.toByte(), // File Identifier of NDEF file
0xFF.toByte(), 0xFE.toByte(), // Maximum NDEF file size of 65534 bytes
0x00.toByte(), // Read access without any security
0xFF.toByte(), // Write access without any security
0x90.toByte(), 0x00.toByte(), // A_OKAY
)
private val NDEF_SELECT_OK = byteArrayOf(
0x00.toByte(), // CLA - Class - Class of instruction
0xa4.toByte(), // Instruction byte (INS) for Select command
0x00.toByte(), // Parameter byte (P1), select by identifier
0x0c.toByte(), // Parameter byte (P1), select by identifier
0x02.toByte(), // Lc field - Number of bytes present in the data field of the command
0xE1.toByte(),
0x04.toByte(), // file identifier of the NDEF file retrieved from the CC file
)
private val NDEF_READ_BINARY = byteArrayOf(
0x00.toByte(), // Class byte (CLA)
0xb0.toByte(), // Instruction byte (INS) for ReadBinary command
)
private val NDEF_READ_BINARY_NLEN = byteArrayOf(
0x00.toByte(), // Class byte (CLA)
0xb0.toByte(), // Instruction byte (INS) for ReadBinary command
0x00.toByte(),
0x00.toByte(), // Parameter byte (P1, P2), offset inside the CC file
0x02.toByte(), // Le field
)
private val A_OKAY = byteArrayOf(
0x90.toByte(), // SW1 Status byte 1 - Command processing status
0x00.toByte(), // SW2 Status byte 2 - Command processing qualifier
)
private val A_ERROR = byteArrayOf(
0x6A.toByte(), // SW1 Status byte 1 - Command processing status
0x82.toByte(), // SW2 Status byte 2 - Command processing qualifier
)
private val NDEF_ID = byteArrayOf(0xE1.toByte(), 0x04.toByte())
private var NDEF_URI = NdefMessage(createTextRecord("en", "Ciao, come va?", NDEF_ID))
private var NDEF_URI_BYTES = NDEF_URI.toByteArray()
private var NDEF_URI_LEN = fillByteArrayToFixedDimension(
BigInteger.valueOf(NDEF_URI_BYTES.size.toLong()).toByteArray(),
2,
)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.hasExtra("ndefMessage")!!) {
NDEF_URI =
NdefMessage(createTextRecord("en", intent.getStringExtra("ndefMessage")!!, NDEF_ID))
NDEF_URI_BYTES = NDEF_URI.toByteArray()
NDEF_URI_LEN = fillByteArrayToFixedDimension(
BigInteger.valueOf(NDEF_URI_BYTES.size.toLong()).toByteArray(),
2,
)
}
Log.i(TAG, "onStartCommand() | NDEF$NDEF_URI")
startForeground(NOTIFICATION_ID, getNotification())
return Service.START_STICKY
}
private val NOTIFICATION_CHANNEL_ID = "your_channel_id"
private val NOTIFICATION_ID = 1
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Your Service Channel",
NotificationManager.IMPORTANCE_LOW
)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
private fun getNotification(): Notification {
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("HCE Service")
.setContentText("Running...")
.setSmallIcon(R.drawable.ic_notification) // Replace with your icon
.setPriority(NotificationCompat.PRIORITY_LOW)
return notificationBuilder.build()
}
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
//
// The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow"
// in the NFC Forum specification
//
Log.i(TAG, "processCommandApdu() | incoming commandApdu: " + commandApdu.toHex())
//
// First command: NDEF Tag Application select (Section 5.5.2 in NFC Forum spec)
//
if (APDU_SELECT.contentEquals(commandApdu)) {
Log.i(TAG, "APDU_SELECT triggered. Our Response: " + A_OKAY.toHex())
return A_OKAY
}
//
// Second command: Capability Container select (Section 5.5.3 in NFC Forum spec)
//
if (CAPABILITY_CONTAINER_OK.contentEquals(commandApdu)) {
Log.i(TAG, "CAPABILITY_CONTAINER_OK triggered. Our Response: " + A_OKAY.toHex())
return A_OKAY
}
//
// Third command: ReadBinary data from CC file (Section 5.5.4 in NFC Forum spec)
//
if (READ_CAPABILITY_CONTAINER.contentEquals(commandApdu) && !READ_CAPABILITY_CONTAINER_CHECK
) {
Log.i(
TAG,
"READ_CAPABILITY_CONTAINER triggered. Our Response: " + READ_CAPABILITY_CONTAINER_RESPONSE.toHex(),
)
READ_CAPABILITY_CONTAINER_CHECK = true
return READ_CAPABILITY_CONTAINER_RESPONSE
}
//
// Fourth command: NDEF Select command (Section 5.5.5 in NFC Forum spec)
//
if (NDEF_SELECT_OK.contentEquals(commandApdu)) {
Log.i(TAG, "NDEF_SELECT_OK triggered. Our Response: " + A_OKAY.toHex())
return A_OKAY
}
if (NDEF_READ_BINARY_NLEN.contentEquals(commandApdu)) {
// Build our response
val response = ByteArray(NDEF_URI_LEN.size + A_OKAY.size)
System.arraycopy(NDEF_URI_LEN, 0, response, 0, NDEF_URI_LEN.size)
System.arraycopy(A_OKAY, 0, response, NDEF_URI_LEN.size, A_OKAY.size)
Log.i(TAG, "NDEF_READ_BINARY_NLEN triggered. Our Response: " + response.toHex())
READ_CAPABILITY_CONTAINER_CHECK = false
return response
}
if (commandApdu.sliceArray(0..1).contentEquals(NDEF_READ_BINARY)) {
val offset = commandApdu.sliceArray(2..3).toHex().toInt(16)
val length = commandApdu.sliceArray(4..4).toHex().toInt(16)
val fullResponse = ByteArray(NDEF_URI_LEN.size + NDEF_URI_BYTES.size)
System.arraycopy(NDEF_URI_LEN, 0, fullResponse, 0, NDEF_URI_LEN.size)
System.arraycopy(
NDEF_URI_BYTES,
0,
fullResponse,
NDEF_URI_LEN.size,
NDEF_URI_BYTES.size,
)
Log.i(TAG, "NDEF_READ_BINARY triggered. Full data: " + fullResponse.toHex())
Log.i(TAG, "READ_BINARY - OFFSET: $offset - LEN: $length")
val slicedResponse = fullResponse.sliceArray(offset until fullResponse.size)
// Build our response
val realLength = if (slicedResponse.size <= length) slicedResponse.size else length
val response = ByteArray(realLength + A_OKAY.size)
System.arraycopy(slicedResponse, 0, response, 0, realLength)
System.arraycopy(A_OKAY, 0, response, realLength, A_OKAY.size)
Log.i(TAG, "NDEF_READ_BINARY triggered. Our Response: " + response.toHex())
READ_CAPABILITY_CONTAINER_CHECK = false
return response
}
//
// We're doing something outside our scope
//
var data = "6F2F840E325041592E5359532E4444463031A51DBF0C1A61184F07A0000000031010870101500A566973612044656269749000"
if(commandApdu.toHex() == "00A404000E325041592E5359532E444446303100"){
return data.hexStringToByteArray()
}
Log.wtf(TAG, "processCommandApdu() | I don't know what's going on!!!")
return A_ERROR
}
override fun onDeactivated(reason: Int) {
Log.i(TAG, "onDeactivated() Fired! Reason: $reason")
}
private val HEX_CHARS = "0123456789ABCDEF".toCharArray()
private fun ByteArray.toHex(): String {
val result = StringBuffer()
forEach {
val octet = it.toInt()
val firstIndex = (octet and 0xF0).ushr(4)
val secondIndex = octet and 0x0F
result.append(HEX_CHARS[firstIndex])
result.append(HEX_CHARS[secondIndex])
}
return result.toString()
}
fun String.hexStringToByteArray(): ByteArray {
val result = ByteArray(length / 2)
for (i in indices step 2) {
val firstIndex = HEX_CHARS.indexOf(this[i])
val secondIndex = HEX_CHARS.indexOf(this[i + 1])
val octet = firstIndex.shl(4).or(secondIndex)
result[i.shr(1)] = octet.toByte()
}
return result
}
private fun createTextRecord(language: String, text: String, id: ByteArray): NdefRecord {
val languageBytes: ByteArray
val textBytes: ByteArray
try {
languageBytes = language.toByteArray(charset("US-ASCII"))
textBytes = text.toByteArray(charset("UTF-8"))
} catch (e: UnsupportedEncodingException) {
throw AssertionError(e)
}
val recordPayload = ByteArray(1 + (languageBytes.size and 0x03F) + textBytes.size)
recordPayload[0] = (languageBytes.size and 0x03F).toByte()
System.arraycopy(languageBytes, 0, recordPayload, 1, languageBytes.size and 0x03F)
System.arraycopy(
textBytes,
0,
recordPayload,
1 + (languageBytes.size and 0x03F),
textBytes.size,
)
return NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, id, recordPayload)
}
private fun fillByteArrayToFixedDimension(array: ByteArray, fixedSize: Int): ByteArray {
if (array.size == fixedSize) {
return array
}
val start = byteArrayOf(0x00.toByte())
val filledArray = ByteArray(start.size + array.size)
System.arraycopy(start, 0, filledArray, 0, start.size)
System.arraycopy(array, 0, filledArray, start.size, array.size)
return fillByteArrayToFixedDimension(filledArray, fixedSize)
}
}
My APDU Config File
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/servicedesc" android:requireDeviceUnlock="false">
<aid-group android:description="@string/aiddescription" android:category="other" >
<aid-filter android:name="D2760000850101"/>
<aid-filter android:name="325041592E5359532E4444463031"/>
<aid-filter android:name="A0000000031010"/>
<aid-filter android:name="A0000000032010"/>
<aid-filter android:name="A0000000032020"/>
<aid-filter android:name="A0000000038010"/>
<!-- GlobalPlatform -->
<!--<aid-filter android:name="FF00000151000001"/>-->
<!--<!– ISO 7816 Applet –>-->
<!--<aid-filter android:name="F276A288BCFBA69D34F31001"/>-->
<!--<!– Joost Applet –>-->
<!--<aid-filter android:name="01020304050601"/>-->
<!--<!– HelloApplet –>-->
<!--<aid-filter android:name="D2760001180002FF49502589C0019B01"/>-->
<!--<aid-filter android:name="F0394148148100" />-->
</aid-group>
<!-- END_INCLUDE(CardEmulationXML) -->
</host-apdu-service>
I need to recieve all the incoming commands
Upvotes: 0
Views: 76
Reputation: 1
I Figured out the issue after installing application I changed the default payment application to my app and start receiving all commands
Setting -> NFC Contactless Payments -> Select your payment App
Upvotes: 0
Reputation: 6414
I'm sorry that I could not detect your workflow as most of your code in "KHostApduService" is code that is used for NDEF processing. You should not leave those old code fragments in your code because that stops to see what your code is doing.
Regarding your app:
In "host-apdu-service" you define the AIDs your HCE emulated Credit Card is "triggered", meaning that Android's OS will forward an incoming request to your app:
<aid-filter android:name="D2760000850101"/>
<aid-filter android:name="325041592E5359532E4444463031"/>
<aid-filter android:name="A0000000031010"/>
<aid-filter android:name="A0000000032010"/>
<aid-filter android:name="A0000000032020"/>
<aid-filter android:name="A0000000038010"/>
<!-- GlobalPlatform -->
<!--<aid-filter android:name="FF00000151000001"/>-->
<!--<!– ISO 7816 Applet –>-->
<!--<aid-filter android:name="F276A288BCFBA69D34F31001"/>-->
<!--<!– Joost Applet –>-->
<!--<aid-filter android:name="01020304050601"/>-->
<!--<!– HelloApplet –>-->
<!--<aid-filter android:name="D2760001180002FF49502589C0019B01"/>-->
<!--<aid-filter android:name="F0394148148100" />-->
For the first contact the EMV Terminal sends this byte sequence to a Credit Card (CC): 325041592E5359532E4444463031
, that is in UTF-8: "2PAY.SYS.DDF01". That means, the terminal is asking the CC what applications are on the tag.
Your APDU service is receiving this request as this byte sequence is included in your Host-Apdu-Service file: <aid-filter android:name="325041592E5359532E4444463031"/>
. The APDU service is responding with this byte sequence: var data = "6F2F840E325041592E5359532E4444463031A51DBF0C1A61184F07A0000000031010870101500A566973612044656269749000"
.
I'm using an Online TLV-Decoder to read the cleartext data:
6F File Control Information (FCI) Template
84 Dedicated File (DF) Name
325041592E5359532E4444463031
A5 File Control Information (FCI) Proprietary Template
BF0C File Control Information (FCI) Issuer Discretionary Data
61 Application Template
4F Application Identifier (AID) – card
A0000000041010
50 Application Label
D e b i t M a s t e r C a r d
87 Application Priority Indicator
01
9F0A Unknown tag
00010101
90 Issuer Public Key Certificate
Your emulated CC is returning one AID to the terminal: A0000000041010
.
The next action from the terminal is to match this received AID with the internal AID-list naming the AIDs the terminal is allowed to process (e.g. a lot of terminals don't process "Amexco" AIDs/Cards as there is no contract to do so. I cannot see if the given AID is allowed from the Terminal side, but if I assume that the Terminal will process this AID. So the Terminal is sending a "Select AID" to the CC, meaning "Select A0000000041010".
What is happening next: a real CC will respond and gives the Terminal additional data, but your emulated CC (or better your APDU processing method) isn't recognizing any request.
The solution is very easy: Why should Android's NFC routing system forward this request to your service ? Please have a close look at the "aid-filter" list in your "host-apdu-service" file, but you won't find any AID that is A0000000041010
and Android is doing what it should do in such a case: nothing.
So there are two ways to solve the problem: use an existing AID from your "host-apdu-service" in the first response - or - add the "unknown" AID to the file.
Upvotes: 0