Kalan's Blog

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

Software Engineer / Taiwanese / Life in Fukuoka
This blog supports RSS feed (all content), you can click RSS icon or setup through third-party service. If there are special styles such as code syntax in the technical article, it is still recommended to browse to the original website for the best experience.

Current Theme light

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

Please notice that currenly most of posts are translated by AI automatically and might contain lots of confusion. I'll gradually translate the post ASAP

How to read SUICA information with CoreNFC

Introduction

Starting from iOS 11.0, you can use CoreNFC to read and write NFC tags. However, it was not possible to read IC card information until iOS 13, when IC card reading was enabled.

I have been interested in NFC for a while and wanted to read the information from Suica (a Japanese transportation IC card) myself, so that I can check the balance directly on my phone (I know there are already apps that can do this). However, there are not many Chinese resources available online regarding Suica reading, so I spent a few days studying the FeliCa documentation and implementing it using CoreNFC.

This article will start with an introduction to NFC protocols, then discuss FeliCa, which is widely used in Japanese transportation IC cards, and finally cover the implementation in Swift. However, please note that I am still new to Swift, so there may be areas where my code is not optimal.

What is NFC?

NFC (Near Field Communication) is a protocol for short-range communication and is also a type of RFID wireless communication technology.

This protocol primarily specifies:

  • Communication protocol: How the transmitter and receiver communicate with each other.
  • Data exchange: How data is exchanged between the two.

This article will focus on the process of reading data from FeliCa.

Problems Solved by NFC

In wireless communication, we can use Bluetooth, Wi-Fi, and other technologies for communication, but the biggest problem lies in security and pairing.

With Bluetooth, for example, you may need to pair devices before they can communicate with each other, which can be cumbersome. If a reader can detect card information from a distance of 20 meters or directly request payment, there are significant security concerns.

Therefore, in NFC, the detection distance is usually within a few centimeters, which not only ensures security but also reduces noise interference.

FeliCa

FeliCa is a contactless IC card technology developed by Sony in 2001. It is faster compared to NFC Type-A and Type-B, possibly due to the overwhelming commuter population in Japan.

By the way, Taiwan's EasyCard is an IC card designed by Philips and manufactured by Mifare. Mifare is also a widely used contactless IC card globally. However, it seems that FeliCa is only widely used in Japan.

FeliCa is incredibly fast. If you have used public transportation in Japan, you may have noticed that you don't even need to stop when entering the platform. The widely used transportation IC cards like Suica, ICOCA, はやかけん, etc., all adopt FeliCa technology. In addition to transportation, these IC cards are also frequently used for payments at convenience stores.

Data Security

In NFC, some data can be read, while others cannot. Some cards require the correct key for decryption in order to read and write data.

Although Android and iOS can now read NFC cards, if you want to modify data such as the balance, you must have the correct key.

FeliCa Architecture

In the FeliCa architecture, it can be mainly divided into two categories: Private Area (プライベート領域) and Common Area.

The Common Area stores some accessible information, while the Private Area stores information such as personal data or balance control, which requires authentication through encryption and decryption.

The Common Area can be further divided into several parts:

  • System: Represents the unit of the card. Each card has a system code (2 bytes), which, according to the Japanese JIS 6319-4 standard, should be between FE00 and AA00. For example, Suica's system code is 0003 (this value is important and will be mentioned later).
  • Area: Contains information such as storage space and the number of blocks for each service.
  • Service: Stores data in blocks for external access. Each service has a service code (2 bytes). For example, the service code for entry and exit records is 090f. Services can be random, cyclic, or pass services.
  • Block: The location where data is stored. Each block is 16 bytes, and the number of blocks required varies depending on the service.

When it comes to simply reading information, the two main parts encountered are the service and block.

Service Types

As mentioned earlier, services can be categorized into random, cyclic, and pass services based on the data access method.

  • Random Service: Allows free read and write access. Contains data determined by the manufacturer.
  • Cyclic Service: Records historical data.
  • Pass Service: Manages balance and payment transactions.

Command Instructions

There are various types of commands in FeliCa, which can be found in the FeliCa documentation. To read FeliCa data, you need a few commands:

  • Polling
  • Request Service
  • Read Without Encryption

Each command has a corresponding request pocket that specifies the required contents. CoreNFC provides functions that handle these requests.

Communication Flow with FeliCa

  1. Capture the card using the polling command.
  2. Use the Request Service command and pass a service code list. The card will verify if the service code is correct and accessible. If the service does not exist or is incorrect, it will return 0xFFFF.
  3. Use the Read Without Encryption command and pass a service code. It will return the corresponding data in blocks (up to 16 blocks).
    1. This command returns status1 and status2. If both are 0, it means there are no issues.
    2. If they are not 0, it indicates an error. There are many possible error codes, but they will not be listed here.

iOS Implementation

To learn how to read NFC tags, you can refer to the example provided by Apple Developer here. Here, we will focus on reading FeliCaTag.

To implement NFC, you first need a physical iPhone, as NFC cannot be read on the simulator.

You can check if NFC is available using NFCTagReaderSession.isReadingAvailable.

First, refer to the CoreNFC documentation and add the corresponding info.plist and entitlement.

1. Add Near Field Communication Tag Reading to Capabilities

ios-capability

2. Add ISO18092 system codes for NFC Tag Reader Session to Info.plist

plist

  • Correspond to the system code mentioned earlier. For example, to read Suica cards, use 0003.
  • Add Privacy - NFC Scan Usage Description.

CoreNFC Implementation

In NFCTagReaderSession, there are already functions provided for polling, requesting services, and reading without encryption. You don't need to manually handle the commands and understand the command bytes.

However, it is still important to understand the overall flow:

  1. Create an NFCTagReadersession and set the pollingOption and delegate.
  2. Call session.begin().
  3. After detecting a card, the session will call the corresponding delegate method.
  4. Implement the reading logic in the func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) function.
    1. Call requestService inside this function.
    2. After confirming the service response, call readWithoutEncryption.

NFCTagReaderSession

To enable a class to have NFC reading capabilities, you need to implement NFCTagReaderSessionDelegate.

public protocol NFCTagReaderSessionDelegate: NSObjectProtocol {
    // Called when the session is ready to read NFC tags.
    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession)

    // Called when the session is invalidated.
    func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error)

    // Called when a tag is detected by the session.
    func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
}

On Apple Developer, you can find the available tag types, including popular ones like MiFare and FeliCa.

Block List

The block list specifies which blocks of a service to read. When using readWithoutEncryption, you need to pass the blockList as a parameter. For the definition of each bit in the blockList, refer to the documentation. In CoreNFC, you can write it like this:

let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) }

Here, 10 represents reading 10 blocks.

FeliCa Reader

To read data from FeliCa, you first need to know the corresponding service codes, such as the service code for boarding history or the service code for card balance. You can find them on this website (in Japanese):

When the System Code is 0003:

  • Service code: 008B (1 block) records card category and balance
  • Service code: 090F (20 blocks) records boarding history (20 entries)
  • Service code: 108F (3 blocks) records transfer history (3 entries)

For me, the boarding history and balance are the most interesting data. Let's see what information is recorded in this block:

Each block contains 16 bytes, and each byte stores corresponding data. Here are a few examples based on the website's documentation. The number represents the byte, and the parentheses indicate the length of the data in bytes.

  • 0 (1 byte): Machine category
  • 1 (1 byte): Usage category (e.g., adjustment, new registration, automatic recharge)
  • 2 (1 byte): Payment method (e.g., credit card, mobile)
  • 3 (1 byte): Entry/exit category (e.g., entry, exit)
  • 4 ~ 5 (2 bytes): Date (year 7 bits, month 4 bits, date 5 bits). The year is based on 2000, so 19 represents 2019.
  • 6 ~ 9 (4 bytes): Station codes for entry/exit, vending machine information
  • A ~ B (2 bytes): Balance (Little Endian)
  • C: Unknown
  • F: Regional code (Kanto private railways, Chubu private railways, Okinawa private railways, etc.)

Knowing this information, we can read the data in the for data in dataList loop. For example, to get the entry/exit time:

let year = data[4] >> 1 // Not sure why right-shifting by 1 is necessary
let month = UInt(bytes: data[4...5]) >> 5 & 0b1111 // Get the month bits
let date = data[5] & 0b11111 // Get the date bits

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.date(from: "20\(year)-\(month)-\(date)")!

Demo

You can find the source code on GitHub.

Afterword

Although I was able to successfully read the information, some of the numbers were different from what I expected. For example, I couldn't find the station code online, no matter how much I searched. I'm not sure if the decoded station code is correct.

Also, although the history records can provide up to 20 entries, reading all of them may fail for some cards. The safer range is around 10 (still testing).

However, after looking at the records (my card is a commuter pass), it seems that not every entry and exit is recorded. If you want to use the card to collect your own entry and exit data, it may not be as useful for visualization, for example.

Regarding FeliCa reading, there is already a handy library written by Japanese developers called TRETJapanNFCReader. If you want to use it directly, you can refer to it. In addition to SUICA, it supports many other types of IC cards, and can even read driver's licenses.

When I saw the CoreNFC SDK, I wanted to try reading FeliCa information. However, there weren't many related Chinese resources available online, although there were many implementations in Japanese. I spent a day reading the FeliCa documentation and trying to implement it.

In addition, Swift type conversion can be a bit tricky, such as converting Data to UInt, etc. I found a code snippet online, but I don't really understand what it's doing. All I know is that I can use UInt(bytes: Data) for conversion.

import Foundation

extension FixedWidthInteger {
    init(bytes: UInt8...) {
        self.init(bytes: bytes)
    }

    init<T: DataProtocol>(bytes: T) {
        let count = bytes.count - 1
        self = bytes.enumerated().reduce(into: 0) { (result, item) in
            result += Self(item.element) << (8 * (count - item.offset))
        }
    }
}

I have been learning iOS development and Swift for a relatively short time, so I may not be aware of some common practices or development methods. Please excuse any messy code.

Prev

Deep Front-End Development

Next

Reading experience: Lateral Leadership

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee