Read the IC balance of your student ID card (Felica) on Android

Introduction

This article is the 8th day article of SLP KBIT Advent Calendar 2019.

Recently, I learned that it is possible to read data on an IC card using the NFC function of Android. So I thought, "Can I read my student ID card?", So I tried it.

About the data to be read

This time, I would like to read the IC balance of the University Cooperatives stored in the student ID card.

The student ID card you have is an IC card compatible with Felica, so you can read it with a terminal compatible with NFC Felica. The Felica standard is summarized in FeliCa Card User's Manual Excerpt.

Regarding the data structure of the student ID card, it was summarized in University Cooperative Felica Specifications, so I referred to it.

The data I want to get this time is

--System code ** 0xFE00 ** --Service code ** 0x50D7 **

It is stored as ** Purse Service ** in. According to the Purse Service specification, the actual balance is stored in the upper 4 bytes in little endian format. Therefore, after retrieving the data, it is necessary to extract 4 bytes and convert it to big endian format.

Roughly summarized, it has the following structure.

- System 0:
    - ...
- System 1: (0xFE00)
    - IDm
    - Area
    - ...
    - Area
        - Service
        - ...
        - Purse Service: (0x50D7) <-here

Reading Felica on Android

The overall process is MainActivity.java, The process of getting data from Felica is done by NfcReader.java.

For the processing performed by MainActivity.java, refer to the following page. [Android] Read NFC: Craft and Horse Racing --Livedoor

The eigenvalues of the cards are made constant in case you want to read another card or data.

MainActivity.java



import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.app.PendingIntent;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.widget.Toast;

import java.nio.ByteBuffer;


public class MainActivity extends AppCompatActivity {

    //constant
    private final byte[] TARGET_SYSTEM_CODE = new byte[]{(byte) 0xFE, (byte) 0x00};
    private final byte[] TARGET_SERVICE_CODE = new byte[]{(byte) 0x50, (byte) 0xD7};
    private final int TARGET_SIZE = 1;

    //Variables for handling adapters
    private NfcAdapter mNfcAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Get an instance of the adapter
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
    }

    @Override
    protected void onResume() {

        super.onResume();

        //Settings when NFC is held over
        Intent intent = new Intent(this, this.getClass());
        intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

        //Avoid opening other apps
        PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null);

    }

    @Override
    protected void onPause() {
        super.onPause();

        //Do not receive when Activity is in the background
        mNfcAdapter.disableForegroundDispatch(this);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        NfcReader nfcReader = new NfcReader();
        super.onNewIntent(intent);

        //Get NFC TAG
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);

        //Read balance data
        byte[][] data = nfcReader.readTag(tag, TARGET_SYSTEM_CODE, TARGET_SERVICE_CODE, TARGET_SIZE);

        //If it cannot be read, it ends
        if (data == null) {
            Toast.makeText(this, "error", Toast.LENGTH_SHORT).show();
            return;
        }

        //conversion
        byte[] balanceData = new byte[]{data[0][3], data[0][2], data[0][1], data[0][0]};
        int balance = ByteBuffer.wrap(balanceData).getInt();

        //display
        Toast.makeText(this, balance + "Circle", Toast.LENGTH_LONG).show();
    }
}

Next, create NfcReader.java. NfcReader.java referred to the code on the following page. Getting Felica (NFC) block data on Android

The processing contents are almost the same, but the readTag method has been changed so that the unique value of the card can be passed as an argument.

NfcReader.java



import android.nfc.Tag;
import android.nfc.tech.NfcF;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

import static android.content.ContentValues.TAG;

class NfcReader {

    public byte[][] readTag(Tag tag, byte[] targetSystemCode, byte[] targetServiceCode, int size) {
        NfcF nfc = NfcF.get(tag);
        if (nfc == null) {
            return null;
        }
        try {
            nfc.connect();

            //Create polling command
            byte[] polling = polling(targetSystemCode);

            //Send a command and get the result
            byte[] pollingRes = nfc.transceive(polling);

            //Get System 0 IDm(The first byte is the data size, the second byte is the response code, and the IDm size is 8 bytes.)
            byte[] targetIDm = Arrays.copyOfRange(pollingRes, 2, 10);

            //Create a Read Without Encryption command
            byte[] req = readWithoutEncryption(targetIDm, size, targetServiceCode);

            //Send a command and get the result
            byte[] res = nfc.transceive(req);

            nfc.close();

            //Parse the result and get only the data
            return parse(res);
        } catch (Exception e) {
            Log.e(TAG, e.getMessage() , e);
        }
        return null;
    }

    /*
     *Get Polling command.
     * @param systemCode byte[]System code to specify
     * @return Polling command
     * @throws IOException
     */
    private byte[] polling(byte[] systemCode) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0x00);           //Data length byte dummy
        bout.write(0x00);           //Command code
        bout.write(systemCode[0]);  // systemCode
        bout.write(systemCode[1]);  // systemCode
        bout.write(0x01);           //Request code
        bout.write(0x0f);           //Time slot

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; //The first byte is the data length
        return msg;
    }

    /*
     *Get the Read Without Encryption command.
     * @param IDm ID of the specified system
     * @param size Number of data to retrieve
     * @return Read Without Encryption command
     * @throws IOException
     */
    private byte[] readWithoutEncryption(byte[] idm, int size, byte[] serviceCode) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0);              //Data length byte dummy
        bout.write(0x06);           //Command code
        bout.write(idm);            // IDm 8byte
        bout.write(1);              //Length of services(The following 2 bytes repeat for this number of minutes)

        //Since the service code is specified as little endian, specify it from the lower byte.
        bout.write(serviceCode[1]); //Service code low byte
        bout.write(serviceCode[0]); //Service code high byte
        bout.write(size);           //Number of blocks

        //Specifying the block number
        for (int i = 0; i < size; i++) {
            bout.write(0x80);       //Block element upper byte "Felica user manual excerpt" 4.See item 3
            bout.write(i);          //Block number
        }

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; //The first byte is the data length
        return msg;
    }

    /*
     *Read Without Encryption Response analysis.
     * @param res byte[]
     * @return string representation
     * @throws Exception
     */
    private byte[][] parse(byte[] res) {
        // res[10]Error code. 0x00 is normal
        if (res[10] != 0x00) {
            throw new RuntimeException("Read Without Encryption Command Error");
        }

        // res[12]Number of response blocks
        // res[13 + n * 16]Actual data 16(byte/block)Repeat
        int size = res[12];
        byte[][] data = new byte[size][16];
        for (int i = 0; i < size; i++) {
            byte[] tmp = new byte[16];
            int offset = 13 + i * 16;
            for (int j = 0; j < 16; j++) {
                tmp[j] = res[offset + j];
            }

            data[i] = tmp;
        }
        return data;
    }
}

I tried using it

When I held my student ID card over my smartphone, the balance was displayed accurately. It matched even when compared with the receipt. Was good.

in conclusion

It was my first time developing on Android, but it was more interesting than I expected. Android has various functions, so I wanted to try various things if I had time.

Recommended Posts

Read the IC balance of your student ID card (Felica) on Android
Read and see the information on the FeliCa (WAON) card
About truncation by the number of bytes of String on Android
(Ruby on Rails6) Display of the database that got the id of the database
Get the acceleration and bearing of the world coordinate system on Android