Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IOException with Host based card emulation

I have been using HCE and have been facing an IOException on

isoDep.connect();

on a specific Android reader device cr100 simcent.

HCE works perfectly fine when I enable the reader mode in NFC with the below flags.

public static int READER_FLAGS =
        NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK;

But with that I am not able to read NDEF tags. Though the same code works perfectly fine on Nexus 7 (2012) Tablet.

The complete code is attached

CardReaderFragment

public class CardReaderFragment extends Fragment implements LoyaltyCardReader.AccountCallback {

    public static final String TAG = "CardReaderFragment";
    // Recommend NfcAdapter flags for reading from other Android devices. Indicates that this
    // activity is interested in NFC-A devices (including other Android devices), and that the
    // system should not check for the presence of NDEF-formatted data (e.g. Android Beam).
    public static int READER_FLAGS =
            NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK;
    public LoyaltyCardReader mLoyaltyCardReader;
    private TextView mAccountField;

    /** Called when sample is created. Displays generic UI with welcome text. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View v = inflater.inflate(R.layout.main_fragment, container, false);
        if (v != null) {
            mAccountField = (TextView) v.findViewById(R.id.card_account_field);
            mAccountField.setText("Waiting...");

            mLoyaltyCardReader = new LoyaltyCardReader(this);

            // Disable Android Beam and register our card reader callback
            enableReaderMode();
        }

        return v;
    }

    @Override
    public void onPause() {
        super.onPause();
        disableReaderMode();
    }

    @Override
    public void onResume() {
        super.onResume();
        enableReaderMode();
    }

    private void enableReaderMode() {
        Log.i(TAG, "Enabling reader mode");
        Activity activity = getActivity();
        NfcAdapter nfc = NfcAdapter.getDefaultAdapter(activity);
        if (nfc != null) {
            nfc.enableReaderMode(activity, mLoyaltyCardReader, READER_FLAGS, null);
        }
    }

    private void disableReaderMode() {
        Log.i(TAG, "Disabling reader mode");
        Activity activity = getActivity();
        NfcAdapter nfc = NfcAdapter.getDefaultAdapter(activity);
        if (nfc != null) {
            nfc.disableReaderMode(activity);
        }
    }

    @Override
    public void onAccountReceived(final String account) {
        // This callback is run on a background thread, but updates to UI elements must be performed
        // on the UI thread.
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mAccountField.setText(account);
            }
        });
    }
}

LoyaltyCardReader

public class LoyaltyCardReader implements NfcAdapter.ReaderCallback {
    private static final String TAG = "LoyaltyCardReader";
    // AID for our loyalty card service.
    private static final String SAMPLE_LOYALTY_CARD_AID = "F222222222";
    // ISO-DEP command HEADER for selecting an AID.
    // Format: [Class | Instruction | Parameter 1 | Parameter 2]
    private static final String SELECT_APDU_HEADER = "00A40400";
    // "OK" status word sent in response to SELECT AID command (0x9000)
    private static final byte[] SELECT_OK_SW = {(byte) 0x90, (byte) 0x00};

    // Weak reference to prevent retain loop. mAccountCallback is responsible for exiting
    // foreground mode before it becomes invalid (e.g. during onPause() or onStop()).
    private WeakReference<AccountCallback> mAccountCallback;

    public interface AccountCallback {
        public void onAccountReceived(String account);
    }

    public LoyaltyCardReader(AccountCallback accountCallback) {
        mAccountCallback = new WeakReference<AccountCallback>(accountCallback);
    }

    /**
     * Callback when a new tag is discovered by the system.
     * <p>
     * <p>Communication with the card should take place here.
     *
     * @param tag Discovered tag
     */
    @Override
    public void onTagDiscovered(Tag tag) {
        Log.i(TAG, "New tag discovered");
        // Android's Host-based Card Emulation (HCE) feature implements the ISO-DEP (ISO 14443-4)
        // protocol.
        //
        // In order to communicate with a device using HCE, the discovered tag should be processed
        // using the IsoDep class.
        IsoDep isoDep = IsoDep.get(tag);
        if (isoDep != null) {
            try {
                // Connect to the remote NFC device
                isoDep.connect();
                // Build SELECT AID command for our loyalty card service.
                // This command tells the remote device which service we wish to communicate with.
                Log.i(TAG, "Requesting remote AID: " + SAMPLE_LOYALTY_CARD_AID);
                byte[] command = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID);
                // Send command to remote device
                Log.i(TAG, "Sending: " + ByteArrayToHexString(command));
                byte[] result = isoDep.transceive(command);
                // If AID is successfully selected, 0x9000 is returned as the status word (last 2
                // bytes of the result) by convention. Everything before the status word is
                // optional payload, which is used here to hold the account number.
                int resultLength = result.length;
                byte[] statusWord = {result[resultLength - 2], result[resultLength - 1]};
                byte[] payload = Arrays.copyOf(result, resultLength - 2);
                if (Arrays.equals(SELECT_OK_SW, statusWord)) {
                    // The remote NFC device will immediately respond with its stored account number
                    String accountNumber = new String(payload, "UTF-8");
                    Log.i(TAG, "Received: " + accountNumber);
                    // Inform CardReaderFragment of received account number
                    mAccountCallback.get().onAccountReceived(accountNumber);
                }
            } catch (IOException e) {
                Log.e(TAG, "Error communicating with card: " + e.toString());
            }
        } else {
            Ndef ndef = Ndef.get(tag);
            if (ndef == null) {
                // NDEF is not supported by this Tag.
                Log.d("NFCCardTagNDEF", "even this is null");
//                return;
            }

            NdefMessage ndefMessage = ndef.getCachedNdefMessage();

            if (ndefMessage == null) {
                Log.d("NFCCardTagNDEF", "ndef message is null");
//                return;
            }

            NdefRecord[] records = ndefMessage.getRecords();
            String text = ndefRecordToString(records[0]);
            Log.d("NFCCardTagNFC", "old" + text);
            mAccountCallback.get().onAccountReceived(text);
        }
    }


    public String ndefRecordToString(NdefRecord record) {
        byte[] payload = record.getPayload();
        return new String(payload);
    }

    /**
     * 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);
    }

    /**
     * Utility class 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];
        int v;
        for (int j = 0; j < bytes.length; j++) {
            v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    /**
     * Utility class to convert a hexadecimal string to a byte string.
     * <p>
     * <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
     */
    public static byte[] HexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }

}

Any help would be appreciated

like image 375
chossen-addict Avatar asked Nov 07 '22 19:11

chossen-addict


1 Answers

Typically, there is not much you could do about this error. An IOException (or the more specific TagLostException) upon IsoDep.connect() indicates that the reading phone could not initiate communication with the HCE device. This is often due to connectivity issues caused by bad coupling (e.g. badly matching antenna dimensions, poor antenna design, antennas partially covered by the device battery without proper antenna design, etc.) or long power-up times (typically only with passive cards and not with HCE). So usually this error is not caused by logical problems with the communication protocol (which you could possibly fix through software) but by problem with the physical characteristics of the devices (which usually require hardware modifications).

Unfortunately, there's not much you can do about this. Possible work-arounds could be:

  1. Try to better place the two phones together (so that the reader device antenna and the HCE device antenna) align to each other and that metal parts of the device case or the device battery does not cover the other device's antenna. In case of significanly different antenna sizes, try to align the other borders of the antennas to each other so that the smaller one is inside the larger one.

  2. On some(!) devices it may help to increase the transceive timeout before calling connect():

    isoDep.setTimeout(10000);
    

    However, it seems that this timeout does not have any effect before connect was called on most devices.

  3. There exist antenna "boosters" for NFC which either detune the HF resonance frequency to get better matching, that mitigate coupling issues do to adapting antenna shape around batteries, or that replace the original antenna altogether. I don't have much experience with this and can't quote any source for such boosters.

EDIT

After re-reading your question, it seems that communication between the cr100 and the HCE device works if you set the flag NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK on the reader device. If that's the case, the IOException might indeed be caused by a logical error. In that case, it could be that the cr100 has a broken NFC discovery implementation that breaks if an ISO-DEP card (e.g. HCE device) does not implement the NDEF tag specification. While I would consider this highly unlikely, you could easily test this by implementing the Type 4 Tag application in your HCE application. See my answer here and the source code here for an example on how to do this.

like image 198
Michael Roland Avatar answered Nov 15 '22 08:11

Michael Roland