如何在 Android 程序中编程使用智能卡读卡器读取智能卡/微处理器卡。

7
最近我一直在处理智能卡,这些卡片包含一些信息。我想要实现的是通过任何Android智能手机使用智能卡读卡器来获取这些智能卡中的数据。我一直在使用HID OMNIKEY 3021 USB智能卡读卡器来读取这些卡片(我知道这个读卡器可以通过Windows应用程序读取这些卡片,因为我已经亲自测试过)。
现在,Android提供了USB Host,可以使安卓智能手机支持读取任何USB Host。
我正在尝试使用USB Host提供的这些类来访问智能卡中的数据。
我的代码用于检测任何USB Host:
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
PendingIntent mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);

IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);

IntentFilter attachedFilter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED);
registerReceiver(mUsbAttachedReceiver, attachedFilter);

private final BroadcastReceiver mUsbAttachedReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Utils.writeStringToTextFile("\n1 .Get an action : " + action, FileName);
        if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
            synchronized (this) {
                device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (device != null) {
                    showToast("Plugged In");
                    mUsbManager.requestPermission(device, mPermissionIntent);
                }
            }
        } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
            UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
            if (device != null) {
                showToast("Plugged Out");
                // call your method that cleans up and closes communication with the device
            }
        }
    }
};

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (ACTION_USB_PERMISSION.equals(action)) {
            synchronized (this) {
                device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    if (device != null) {
                        //call method to set up device communication
                        Utils.writeStringToTextFile("2 .Get an action : " + action + "\nDevice is : " + device, FileName);
                        showToast("Permission Granted for device");

                        Handler h = new Handler();
                        h.postDelayed(run, 1000);

                    }
                } else {
                    showToast("Permission denied for device" + device);
                }
            }
        }
    }
};

当我获取到UsbDevice device时,一切都按预期运行,它提供了设备的信息,例如:

Device is : UsbDevice[mName=/dev/bus/usb/001/002,mVendorId=1899,mProductId=12322,mClass=0,mSubclass=0,mProtocol=0,mManufacturerName=OMNIKEY AG,mProductName=Smart Card Reader USB,mVersion=2.0,mSerialNumber=null,mConfigurations=[
UsbConfiguration[mId=1,mName=CCID,mAttributes=160,mMaxPower=50,mInterfaces=[
UsbInterface[mId=0,mAlternateSetting=0,mName=null,mClass=11,mSubclass=0,mProtocol=0,mEndpoints=[
UsbEndpoint[mAddress=131,mAttributes=3,mMaxPacketSize=8,mInterval=24]
UsbEndpoint[mAddress=132,mAttributes=2,mMaxPacketSize=64,mInterval=0]
UsbEndpoint[mAddress=5,mAttributes=2,mMaxPacketSize=64,mInterval=0]]]]

现在我正在尝试使用UsbDevice device从卡中提取数据和详细信息,但我没有成功,并且我找不到任何有用的帖子。
我知道我必须使用UsbInterfaceUsbEndpointUsbDeviceConnection来获取我想要的东西,但我无法做到。
另外,我也找不到任何示例或类似的东西。有人能指点我一下吗?
对不起,帖子太长了,先谢谢:)
编辑: 感谢Mr. Michael Roland,我能够阅读关于CCID的内容,因为读卡器设备通过USB接口使用CCID。
所以我使用了以下代码:
        UsbDeviceConnection connection = mUsbManager.openDevice(device);
        UsbEndpoint epOut = null, epIn = null;

        for (int i = 0; i < device.getInterfaceCount(); i++) {
            UsbInterface usbInterface = device.getInterface(i);
            connection.claimInterface(usbInterface, true);

            for (int j = 0; j < usbInterface.getEndpointCount(); j++) {
                UsbEndpoint ep = usbInterface.getEndpoint(j);
                showToast("Endpoint is : " + ep.toString() + " endpoint's type : " + ep.getType() + " endpoint's direction : " + ep.getDirection());
                Log.d(" ", "EP " + i + ": " + ep.getType());
                if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                    if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
                        epOut = ep;

                    } else if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                        epIn = ep;
                    }

                }
            }

            int dataTransferred = 0;
            byte[] PC_to_RDR_IccPowerOn = hexStringToByteArray("62" + "00000000" + "00" + "00" + "00" + "0000");

            if (epOut != null) {
                //Firstly send Power in on Bulk OUT endpoint
                dataTransferred = connection.bulkTransfer(epOut, PC_to_RDR_IccPowerOn, PC_to_RDR_IccPowerOn.length, TIMEOUT);
            }

            StringBuilder result = new StringBuilder();

            if (epIn != null) {
                final byte[] RDR_to_PC_DataBlock = new byte[epIn.getMaxPacketSize()];
                result = new StringBuilder();
                //Secondly send Power out on Bulk OUT endpoint
                dataTransferred = connection.bulkTransfer(epIn, RDR_to_PC_DataBlock, RDR_to_PC_DataBlock.length, TIMEOUT);
                for (byte bb : RDR_to_PC_DataBlock) {
                    result.append(String.format(" %02X ", bb));
                }

                if (dataTransferred > 0) {
                    Utils.writeStringToTextFile("\n2nd buffer received was : " + result.toString(), "Card_communication_data.txt");
                    String s1 = Arrays.toString(RDR_to_PC_DataBlock);
                    String s2 = new String(RDR_to_PC_DataBlock);
                    showToast("received - " + s1 + " - " + s2);
                } else {
                    showToast("received length at 2nd buffer transfer was " + dataTransferred);
                }
            }
        }

我收到了80 13 00 00 00 00 00 00 00 00 3B 9A 96 C0 10 31 FE 5D 00 64 05 7B 01 02 31 80 90 00 76 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00,但我仍然不确定如何处理数据字段:ATR,或者如何为PC_to_RDR_XfrBlock命令组成Command APDU
我认为我应该现在将命令APDU包装到PC_to_RDR_XfrBlock命令中发送; 有人能帮我吗?

编辑2:我弄清楚了ATR的含义和如何形成命令APDU。

但是现在我应该切换协议。

默认协议是T=0。为了设置T=1协议,设备必须向卡发送PTS(也称为PPS)。 由于卡需要同时支持T=0和T=1协议,因此协议切换的基本PTS对于卡是必需的。 PTS可以用于根据ISO/IEC 7816-3中的指示,在存在任何(TA(1)字节)的情况下,切换到比卡在ATR中提出的默认波特率更高的波特率。

我不确定这意味着什么,以及如何实现它!

2个回答

11

由于没有适当的指南或任何一个可以遵循的基本步骤样本,所以这里是我是如何进行通信的(这更像是新手指南,请纠正我如果我错了): 首先,使用USB Host API,我能够通过智能卡读卡器连接到智能卡。

关于连接,这里是一小段代码片段来帮助你理解:

    //Allows you to enumerate and communicate with connected USB devices.
    UsbManager mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
    //Explicitly asking for permission
    final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
    PendingIntent mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
    HashMap<String, UsbDevice> deviceList = mUsbManager.getDeviceList();

    UsbDevice device = deviceList.get("//the device you want to work with");
    if (device != null) {
        mUsbManager.requestPermission(device, mPermissionIntent);
    }

现在您需要了解的是,在Java中,通信是使用package javax.smartcard进行的,但Android不支持该包,因此请参考这里以了解如何通信或发送/接收APDU(智能卡命令)。
现在如上面提到的答案所述,

您不能简单地通过批量输出端点发送APDU(智能卡命令),并期望通过批量输入端点接收响应APDU。

要获取端点,请参阅下面的代码片段:
UsbEndpoint epOut = null, epIn = null;
UsbInterface usbInterface;

UsbDeviceConnection connection = mUsbManager.openDevice(device);

        for (int i = 0; i < device.getInterfaceCount(); i++) {
            usbInterface = device.getInterface(i);
            connection.claimInterface(usbInterface, true);

            for (int j = 0; j < usbInterface.getEndpointCount(); j++) {
                UsbEndpoint ep = usbInterface.getEndpoint(j);

                if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                    if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
                        // from host to device
                        epOut = ep;

                    } else if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                        // from device to host
                        epIn = ep;
                    }
                }
            }
        }

现在你有了批量输入和批量输出端点来发送和接收APDU命令和APDU响应块:
对于发送命令,请参见下面的代码片段:
public void write(UsbDeviceConnection connection, UsbEndpoint epOut, byte[] command) {
    result = new StringBuilder();
    connection.bulkTransfer(epOut, command, command.length, TIMEOUT);
    //For Printing logs you can use result variable
    for (byte bb : command) {
        result.append(String.format(" %02X ", bb));
    }
}

接收/读取响应的代码段如下:

public int read(UsbDeviceConnection connection, UsbEndpoint epIn) {
result = new StringBuilder();
final byte[] buffer = new byte[epIn.getMaxPacketSize()];
int byteCount = 0;
byteCount = connection.bulkTransfer(epIn, buffer, buffer.length, TIMEOUT);

    //For Printing logs you can use result variable
    if (byteCount >= 0) {
        for (byte bb : buffer) {
            result.append(String.format(" %02X ", bb));
        }

        //Buffer received was : result.toString()
    } else {
        //Something went wrong as count was : " + byteCount
    }

    return byteCount;
}

现在,如果您看到这个答案,将要发送的第一个命令是:

PC_to_RDR_IccPowerOn 命令以激活卡片。

您可以通过阅读此处的USB设备类规范文档6.1.1节来创建该命令。

现在让我们以此命令为例,如下所示:62000000000000000000 您可以这样发送它:

write(connection, epOut, "62000000000000000000");

现在,成功发送APDU命令后,您可以使用以下方法读取响应:

read(connection, epIn);

现在你会收到类似以下的响应:

80 18000000 00 00 00 00 00 3BBF11008131FE45455041000000000000000000000000F1

上面代码中的响应将会在之前代码中的read()方法的result变量中,你可以从同一个RDR_to_PC_DataBlock中获取Data field: ATR

你需要了解/阅读如何读取这个ATR,它可以用于检测卡片类型和其他信息,例如它是否使用T0或T1协议,然后TA(1)可以告诉你有关FI Index into clock conversion factor tableDI Index into Baud rate adjustment factor table等内容。

查看此网站以解析ATR

现在,如果你想切换协议,例如从T0切换到T1,即发送PPS/PTS或者你想设置任何参数,则可以使用PC_to_RDR_SetParameters命令(文档6.1.7节)。

在我这种情况下,用于从 T0 切换到 T1PPS/PTS 示例是: "61 00000007 00 00 01 0000 9610005D00FE00"

这将得到一些结果,例如:"82 07000000 00 00 00 00 01 96 10 00 5D 00 FE 00",您可以使用RDR_to_PC_Parametersdocument的6.2.3节)进行检查。

有关此命令的详细信息,请参见CCID协议文档的第6.1.7节。 我能够使用从 ATR 块收到的详细信息形成此命令,然后使用write()方法发送命令,再使用read()方法读取响应。

此外,要选择任何文件或发送任何命令,可以使用PC_to_RDR_XfrBlock以编程方式使用write()方法发送,然后使用read()方法接收响应。您还可以更改在read()方法中要读取的字节数。

记得使用线程进行通信,还可以阅读此处获取更多提示。


一篇有用的帖子,谢谢。我也正在尝试使用OmniKey。我只对在读卡器上扫描他们的卡时收到通知感兴趣。我已经按照您的实现进行了操作。但是,我无法读取任何内容。结果为-1。还有一件事,在读卡器上尝试几次换卡后,读卡器停止响应换卡(没有蜂鸣声或蓝色光)。请问是否有方法解决此问题?谢谢。 - Abdulkarim Kanaan
@AbdulkarimKanaan关于交换问题,我不确定。至于通知,请检查Android是否提供了任何系统广播。如果没有,您将不得不使用USB管理器并继续查询USBManager中是否存在任何设备(请查看答案的第一部分)。 - shadygoneinsane
你觉得我需要使用线程来读取吗? - Abdulkarim Kanaan
还有一件事,写入命令 write(connection, epOut, "62000000000000000000") 返回 36 32 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 - Abdulkarim Kanaan
@AbdulkarimKanaan,我已经有一段时间没有处理这个问题了,所以我不确定确切的命令是什么。请在文档CCID协议中查看。 - shadygoneinsane
@shadygoneinsane,看起来你正在读取卡车司机的记录仪卡。你最终是使用了CCID驱动程序还是自己实现了CCID层? - John Reynolds

1
典型的USB智能卡读卡器实现了USB CCID设备类规范。因此,您需要在应用程序中实现该协议才能与读卡器(和卡片)通信。请参见如何通过Android USB主机与智能卡读卡器通信的起点Communicate with smartcard reader through Android USB host(部分工作)。请注意,保留HTML标签。

谢谢指引方向,但是很难弄清楚哪个命令用于什么目的。 例如 PTS/PPS - 关于 6.1.7 PC_to_RDR_SetParameters ,可以在 CCID 规范文档中参考。 - shadygoneinsane
@shadygoneinsane 如果您有一个可行的解决方案,如果您能在自己的答案中分享,那将是非常好的。 - Michael Roland
2
无论如何,这将是一个很长的答案,我会尝试在这里发布它。 - shadygoneinsane

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接