主机卡仿真仅接收来自终端的一个命令

问题描述 投票:0回答:1

大家好,我正在创建一个应用程序,它将模仿 emv 卡并将数据返回到 POS 终端 问题 收到 PPSE 命令后,我无法接收第二个传入命令,并且我的服务因原因 1 而停用

我的清单文件

<?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>

我的服务康德

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)
    }


}

我的 APDU 配置文件

<?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"/>-->
        <!--&lt;!&ndash; ISO 7816 Applet &ndash;&gt;-->
        <!--<aid-filter android:name="F276A288BCFBA69D34F31001"/>-->
        <!--&lt;!&ndash; Joost Applet &ndash;&gt;-->
        <!--<aid-filter android:name="01020304050601"/>-->
        <!--&lt;!&ndash; HelloApplet &ndash;&gt;-->
        <!--<aid-filter android:name="D2760001180002FF49502589C0019B01"/>-->
        <!--<aid-filter android:name="F0394148148100" />-->
    </aid-group>
    <!-- END_INCLUDE(CardEmulationXML) -->
</host-apdu-service>

我需要接收所有传入的命令

nfc emv hce
1个回答
0
投票

很抱歉,我无法检测到您的工作流程,因为“KHostApduService”中的大部分代码都是用于 NDEF 处理的代码。您不应该将那些旧的代码片段留在代码中,因为这样会停止查看您的代码在做什么。

关于您的应用程序:

在“host-apdu-service”中,您定义“触发”HCE 模拟信用卡的 AID,这意味着 Android 操作系统会将传入请求转发到您的应用程序:

    <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"/>-->
    <!--&lt;!&ndash; ISO 7816 Applet &ndash;&gt;-->
    <!--<aid-filter android:name="F276A288BCFBA69D34F31001"/>-->
    <!--&lt;!&ndash; Joost Applet &ndash;&gt;-->
    <!--<aid-filter android:name="01020304050601"/>-->
    <!--&lt;!&ndash; HelloApplet &ndash;&gt;-->
    <!--<aid-filter android:name="D2760001180002FF49502589C0019B01"/>-->
    <!--<aid-filter android:name="F0394148148100" />-->

对于第一次联系,EMV 终端会将此字节序列发送到信用卡 (CC):

325041592E5359532E4444463031
,即 UTF-8 格式:“2PAY.SYS.DDF01”。这意味着,终端正在询问 CC 标签上有哪些应用程序。

您的 APDU 服务正在接收此请求,因为此字节序列包含在您的 Host-Apdu-Service 文件中:

<aid-filter android:name="325041592E5359532E4444463031"/>
。 APDU 服务使用以下字节序列进行响应:
var data = "6F2F840E325041592E5359532E4444463031A51DBF0C1A61184F07A0000000031010870101500A566973612044656269749000"

我正在使用在线 TLV 解码器来读取明文数据:

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

您的模拟 CC 正在向终端返回一个 AID:

A0000000041010

终端的下一个操作是将收到的 AID 与内部 AID 列表进行匹配,该列表命名允许终端处理的 AID(例如,许多终端不处理“Amexco”AID/卡,因为没有合同我看不到终端端是否允许给定的 AID,但如果我假设终端将处理此 AID,那么终端将向 CC 发送“选择 AID”,意思是“选择 A0000000041010”。

接下来会发生什么:真正的 CC 将响应并为终端提供附加数据,但您的模拟 CC(或更好的 APDU 处理方法)无法识别任何请求。

解决方案非常简单:为什么 Android 的 NFC 路由系统应该将此请求转发给您的服务?请仔细查看“host-apdu-service”文件中的“aid-filter”列表,但您不会找到任何

A0000000041010

 的 AID,并且 Android 正在执行在这种情况下应该执行的操作:什么也没有。

因此有两种方法可以解决该问题:在第一个响应中使用“host-apdu-service”中的现有 AID - 或者 - 将“未知”AID 添加到文件中。

© www.soinside.com 2019 - 2024. All rights reserved.