NFC读卡器和安卓手机

4
这是我第一次发问题,所以请耐心等待。我正在使用运行Android 4.4.2的三星Galaxy S4开发NFC应用程序。该手机肯定能够使用基于主机的卡模拟(HCE),我想利用这一点与处于读卡器/写卡器模式下的ACR1252U-A1 NFC读卡器(Advanced Card Systems)进行通信。
读卡器连接到PC上,我使用javax.smartcardio库编写了Java应用程序来与读卡器通信。到目前为止,我已经能够从读卡器向手机发送SELECT命令,接收手机的响应并在读卡器和手机之间发送后续消息。在Android端,我扩展了Android HCE API中的HostApduService类。我基本上是在玩硬件,并决定创建一个Android应用程序,然后将一些会员卡信息发送到POS系统,该系统反过来将柜台号码发送到设备。
然而,设备之间的通信似乎只有在手机被锁定时才能正常工作。如果我解锁手机(主屏幕或其他任何内容),我的电脑会尝试安装称为“智能卡”的驱动程序,但失败了(如预期)或者它根本无法连接到手机。基本上,我希望Java应用程序在手机解锁和锁定时都能正常工作。
这是我Java应用程序的主要方法:
public static void main(String[] args) {
    try {
        TerminalFactory factory = TerminalFactory.getDefault();
        List terminals = factory.terminals().list();
        System.out.println("Terminals count: " + terminals.size());
        System.out.println("Terminals: " + terminals);

        // Get the first terminal in the list
        CardTerminal terminal = (CardTerminal) terminals.get(0);
        System.out.println("Using terminal: " + terminal);
        System.out.println("Waiting for card present...");
        terminal.waitForCardPresent(2000);
        if (terminal.isCardPresent()) {
            System.out.println("Card present!");
        }
        // Establish a connection with the card using
        // "T=0", "T=1", "T=CL" or "*"
        Card card = terminal.connect("*");
        System.out.println("Card: " + card);

        // Get ATR
        byte[] baATR = card.getATR().getBytes();
        System.out.println("ATR: " + TestSmartCardIO.toString(baATR));

        CardChannel channel = card.getBasicChannel();

        // Setup terminal device settings (i.e. buzzer and LED)
        byte[] data = { (byte) 0xE0, (byte) 0x00, (byte) 0x00, (byte) 0x21,
                (byte) 0x01, (byte) 0x77 };
        System.out.println("Setting up terminal device...");
        card.transmitControlCommand(
                IOCTL_SMARTCARD_ACR1251_ACR1252_ESCAPE_COMMAND, data);

        /*
         * SELECT Command See GlobalPlatform Card Specification (e.g. 2.2,
         * section 11.9) CLA: 00 INS: A4 P1: 04 i.e. b3 is set to 1, means
         * select by name P2: 00 i.e. first or only occurence Lc: 08 i.e.
         * length of AID see below Data: A0 00 00 00 03 00 00 00 AID of the
         * card manager
         */
        // Create select to select the correct Android application.
        System.out.println("Sending SELECT command...");
        byte[] selectAidApdu = createSelectAidApdu(AID_ANDROID);
        System.out.println("APDU >>: "
                + TestSmartCardIO.toString(selectAidApdu));
        ResponseAPDU response = channel.transmit(new CommandAPDU(
                selectAidApdu));
        System.out.println("APDU <<: "
                + TestSmartCardIO.toString(response.getBytes()));

        // Check response to ensure successful.
        if (response.getSW() == SW_OK) {
            System.out.println("Selection successful.");
            String ssNumber = new String(response.getData());
            System.out.println("SS Number : " + ssNumber);
            // Send another message to device.
            System.out.println("Sending Till number.");

            byte[] message = { (byte) 0x00, (byte) TILL_ID };
            byte[] messageAidApdu = createMessageApdu(message);
            System.out.println("APDU >>: "
                    + TestSmartCardIO.toString(messageAidApdu));
            response = channel.transmit(new CommandAPDU(messageAidApdu));
            if (response.getSW() == SW_OK) {
                System.out.println("APDU <<: "
                        + TestSmartCardIO.toString(response.getBytes()));
                String ack = new String(response.getData());
                System.out.println("Received : " + ack);
            } else {
                System.out.println("SW1: " + response.getSW1());
                System.out.println("SW2: " + response.getSW2());
            }

        } else {
            System.out.println("SW1: " + response.getSW1());
            System.out.println("SW2: " + response.getSW2());
        }
        // Disconnect
        // true: reset the card after disconnecting card.
        card.disconnect(true);
    } catch (CardException e) {
        e.printStackTrace();
    }
}

这是我的 Android 设备上的服务:

import java.util.Arrays;

import android.content.Intent;
import android.content.SharedPreferences;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.util.Log;

public class MyHostApduService extends HostApduService {
private static final String TAG = "CardService";
// AID for our loyalty card service.
private static final String SAMPLE_LOYALTY_CARD_AID = "F0010203040506";
// ISO-DEP command HEADER for selecting an AID.
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
private static final String SELECT_APDU_HEADER = "00A40400";
private static final String PUT_DATA_APDU_HEADER = "00DA0000";
// "OK" status word sent in response to SELECT AID command (0x9000)
private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000");
// "UNKNOWN" status word sent in response to invalid APDU command (0x0000)
private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000");
private static final byte[] SELECT_APDU = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID);
public static final String PREFS_NAME = "MyPrefsFile";
public static final String SS_NUMBER = "ssNumber";
public static final String TILL_NUMBER = "tillNumber";

/**
 * Called if the connection to the NFC card is lost, in order to let the
 * application know the cause for the disconnection (either a lost link, or
 * another AID being selected by the reader).
 * 
 * @param reason
 *            Either DEACTIVATION_LINK_LOSS or DEACTIVATION_DESELECTED
 */
@Override
public void onDeactivated(int reason) {
}

/**
 * This method will be called when a command APDU has been received from a
 * remote device. A response APDU can be provided directly by returning a
 * byte-array in this method. In general response APDUs must be sent as
 * quickly as possible, given the fact that the user is likely holding his
 * device over an NFC reader when this method is called.
 * 
 * <p class="note">
 * If there are multiple services that have registered for the same AIDs in
 * their meta-data entry, you will only get called if the user has
 * explicitly selected your service, either as a default or just for the
 * next tap.
 * 
 * <p class="note">
 * This method is running on the main thread of your application. If you
 * cannot return a response APDU immediately, return null and use the
 * {@link #sendResponseApdu(byte[])} method later.
 * 
 * @param commandApdu
 *            The APDU that received from the remote device
 * @param extras
 *            A bundle containing extra data. May be null.
 * @return a byte-array containing the response APDU, or null if no response
 *         APDU can be sent at this point.
 */
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
    Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu));
    // Copy command section to determine type of command.
    byte[] command = new byte[4];
    System.arraycopy(commandApdu, 0, command, 0, 4);

    // If the APDU matches the SELECT AID command for this service,
    // send the loyalty card account number.
    Log.i(TAG, "Command String: " + ByteArrayToHexString(command));
    if (Arrays.equals(SELECT_APDU, commandApdu)) {
        // Retrieve stored SS number.
        SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
        String account = settings.getString(SS_NUMBER, "00000000");

        byte[] accountBytes = account.getBytes();
        Log.i(TAG, "Application Selected. Sending account number: "
                + account);
        return ConcatArrays(accountBytes, SELECT_OK_SW);
    } else if (PUT_DATA_APDU_HEADER.equals(ByteArrayToHexString(command))) {
        int dataLength = commandApdu[4];
        byte[] data = new byte[dataLength];
        System.arraycopy(commandApdu, 5, data, 0, dataLength);
        int tillNumber = Integer.parseInt(ByteArrayToHexString(data), 16);
        Log.i(TAG, "Till Number: " + tillNumber);
        String ack = "ACK";
        byte[] ackBytes = ack.getBytes();
        Intent i = new Intent();
        i.setClass(this, MainActivity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.putExtra(TILL_NUMBER, tillNumber);
        startActivity(i);
        return ConcatArrays(ackBytes, SELECT_OK_SW);
    } else {
        return UNKNOWN_CMD_SW;
    }
}

/**
 * Build APDU for SELECT AID command. This command indicates which service a
 * reader is interested in communicating with. See ISO 7816-4.
 * 
 * @param aid
 *            Application ID (AID) to select
 * @return APDU for SELECT AID command
 */
public static byte[] BuildSelectApdu(String aid) {
    // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH |
    // DATA]
    return HexStringToByteArray(SELECT_APDU_HEADER
            + String.format("%02X", aid.length() / 2) + aid);
}

public static byte[] BuildCommandApdu(String command) {
    // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH |
    // DATA]

    return HexStringToByteArray(command);
}

/**
 * Utility method to convert a byte array to a hexadecimal string.
 * 
 * @param bytes
 *            Bytes to convert
 * @return String, containing hexadecimal representation.
 */
public static String ByteArrayToHexString(byte[] bytes) {
    final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'A', 'B', 'C', 'D', 'E', 'F' };
    char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex
                                                    // characters (nibbles)
    int v;
    for (int j = 0; j < bytes.length; j++) {
        v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned
                                // value
        hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from
                                                // upper nibble
        hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character
                                                    // from lower nibble
    }
    return new String(hexChars);
}

/**
 * Utility method to convert a hexadecimal string to a byte string.
 * 
 * <p>
 * Behavior with input strings containing non-hexadecimal characters is
 * undefined.
 * 
 * @param s
 *            String containing hexadecimal characters to convert
 * @return Byte array generated from input
 * @throws java.lang.IllegalArgumentException
 *             if input length is incorrect
 */
public static byte[] HexStringToByteArray(String s)
        throws IllegalArgumentException {
    int len = s.length();
    if (len % 2 == 1) {
        throw new IllegalArgumentException(
                "Hex string must have even number of characters");
    }
    byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
    for (int i = 0; i < len; i += 2) {
        // Convert each character into a integer (base-16), then bit-shift
        // into place
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character
                .digit(s.charAt(i + 1), 16));
    }
    return data;
}

/**
 * Utility method to concatenate two byte arrays.
 * 
 * @param first
 *            First array
 * @param rest
 *            Any remaining arrays
 * @return Concatenated copy of input arrays
 */
public static byte[] ConcatArrays(byte[] first, byte[]... rest) {
    int totalLength = first.length;
    for (byte[] array : rest) {
        totalLength += array.length;
    }
    byte[] result = Arrays.copyOf(first, totalLength);
    int offset = first.length;
    for (byte[] array : rest) {
        System.arraycopy(array, 0, result, offset, array.length);
        offset += array.length;
    }
    return result;
}
}

您可能还想查看我的aid.xml文件以查看我的AID编号:

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/service_name"
    android:requireDeviceUnlock="false">
    <aid-group android:description="@string/SS_title" android:category="other">
        <aid-filter android:name="F0010203040506"/>
    </aid-group>
</host-apdu-service>

这是Java应用程序成功连接的输出日志:
Terminals count: 2
Terminals: [PC/SC terminal ACS ACR1252 1S CL Reader PICC 0, PC/SC terminal     
ACS ACR12521S CL Reader SAM 0]
Using terminal: PC/SC terminal ACS ACR1252 1S CL Reader PICC 0
Waiting for card present...
Card present!
Card: PC/SC card in ACS ACR1252 1S CL Reader PICC 0, protocol T=1, state OK
ATR: 3B808011
Setting up terminal device...
Sending SELECT command...
APDU >>: 0A4407F0123456
APDU <<: 313132323333343435900
Selection successful.
SS Number : 112233445
Sending Till number.
APDU >>: 0DA00203
APDU <<: 41434B900
Received : ACK

这是Java应用程序连接失败的输出日志:
Terminals count: 2
Terminals: [PC/SC terminal ACS ACR1252 1S CL Reader PICC 0, PC/SC terminal ACS 
ACR1252 1S CL Reader SAM 0]
Using terminal: PC/SC terminal ACS ACR1252 1S CL Reader PICC 0
Waiting for card present...
Card present!
Card: PC/SC card in ACS ACR1252 1S CL Reader PICC 0, protocol T=1, state OK
ATR: 3B8F801804FCA000361103B000042
Setting up terminal device...
Sending SELECT command...
APDU >>: 0A4407F0123456
APDU <<: 641
SW1: 100
SW2: 1

如果还有其他我可以做的事情,或者我漏掉了一些可以帮助回答我的问题的内容,请告诉我。谢谢!


你尝试过使用android:requireDeviceUnlock="true"吗? - Rachita Nanda
1个回答

2

虽然这是一篇旧文章,但它仍可能对某些人有参考价值。在SELECT命令中发生了某些事情,导致填充零被删除。

SELECT命令如下:

0A4407F0123456

虽然应该是这样的(没有空格,只是为了更容易看出区别)
00 A4 04 00 07 F0 01 02 03 04 05 06

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