· 10 min read

How to Read SUICA Information with CoreNFC (FeliCa)

This article was auto-translated from Chinese. Some nuances may be lost in translation.

Introduction

Starting from iOS 11.0, developers could read and write NFC tags via CoreNFC, but reading IC cards wasn’t supported until iOS 13 opened up IC card reading.

I’ve always been interested in NFC and wanted to try reading Suica (a Japanese transit IC card) data myself, so that I could check my balance directly with my phone (I know there are already apps that do this). However, there are relatively few Chinese resources online about reading Suica, so I spent a few days studying the FeliCa documentation and implementing it using CoreNFC.

This article starts with the NFC protocol, moves on to FeliCa—which is widely used in Japanese transit IC cards—and finally covers the implementation in Swift. Since I’m still relatively new to Swift, some parts of the code might not be optimal.

What is NFC?

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

This protocol mainly defines:

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

This article focuses on the process of reading data from FeliCa.

The Problem NFC Solves

For wireless communication, we can use Bluetooth, Wi-Fi, etc., but the biggest challenges lie in security and pairing.

Bluetooth often requires pairing before devices can talk to each other, making the setup somewhat tedious. Furthermore, if a card reader could detect card information or request payments from 20 meters away, it would pose significant security risks.

Therefore, in NFC, the detection distance is typically within a few centimeters. In addition to ensuring security, this also minimizes noise and interference.

FeliCa

FeliCa is a contactless IC card technology developed by Sony in 2001. It is faster than NFC Type-A and Type-B, likely owing to Japan’s massive commuter crowds.

As a side note, Taiwan’s EasyCard is an IC card made using Mifare, designed by Philips. Mifare is also widely used around the world for contactless IC cards, whereas FeliCa seems to be predominantly adopted in Japan.

FeliCa is remarkably fast. If you’ve taken public transit in Japan, you’ve probably noticed that people don’t need to pause at all when passing through ticket gates. Widely used transit IC cards such as Suica, ICOCA, and Hayakaken all adopt FeliCa technology. Beyond transit, these IC cards are also frequently used for payments at convenience stores.

Data Security

In NFC, some data is readable while some is not; certain cards require the correct decryption keys to read and write data.

Although both Android and iOS can now read NFC cards, tampering with data like balances still requires valid cryptographic keys.

FeliCa Architecture

The FeliCa architecture is broadly divided into two areas: the Private Area (プライベート領域) and the Common Area (共通領域).

The Common Area stores readable information, while the Private Area holds personal details or balance controls, requiring cryptographic authentication to access.

The Common Area is further divided into several components:

  • System: Represents the card unit. Every card has a 2-byte system code. According to Japan’s JIS 6319-4 standard, this value falls between FE00 and AA00. For cards like Suica, it is 0003 (this value is crucial and will appear later).
  • Area: Contains information such as storage space and the number of blocks allocated to each service.
  • Service: Stores data in blocks for external access. Each service has a 2-byte service code; for example, the transit history service code is 090f. Services are categorized into Random Service, Cyclic Service, and Pass Service.
  • Block: Where the actual data is stored. Each block is 16 bytes, and the required number of blocks varies depending on the service.

If you are just reading data, the parts you’ll encounter most often are the service and the block.

Types of Services

As mentioned earlier, services are categorized into Random, Cyclic, and Pass, primarily based on their data access patterns:

  • Random Service: Free read/write access, holding data defined by the manufacturer.
  • Cyclic Service: Stores historical/log data.
  • Pass Service: Manages balances, reloads, and deductions.

Commands

FeliCa supports various commands, which can be found in the FeliCa documentation. To read FeliCa data, several commands are required:

  • Polling
  • Request Service
  • Read Without Encryption

Each command has a corresponding request packet specifying what the request must contain. CoreNFC already provides built-in methods wrapping these commands.

FeliCa Communication Flow

  1. Capture the card using the Polling command.
  2. Send the Request Service command with a list of service codes. The card verifies whether the service codes are valid and accessible. If a service does not exist or is invalid, it returns 0xFFFF.
  3. Send the Read Without Encryption command along with the service code. The card returns the corresponding data in blocks (up to 16 services).
    1. This command returns status1 and status2. If both are 0, the operation succeeded.
    2. Non-zero values indicate errors; since there are quite a few error codes, I won’t list them all here.

iOS Implementation

For instructions on reading general NFC tags, refer to Apple Developer’s sample code. Here, we will focus specifically on how to read a FeliCaTag.

To test NFC, you must use a physical iPhone, as the iOS Simulator cannot read NFC.

You can check whether NFC reading is supported using NFCTagReaderSession.isReadingAvailable.

First, consult the CoreNFC documentation to configure your Info.plist and entitlements.

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

  • This corresponds to the system code mentioned earlier; for cards like Suica, it is 0003.
  • Add Privacy - NFC Scan Usage Description.

CoreNFC Implementation

In NFCTagReaderSession, methods for polling, requestService, and readWithoutEncryption are already wrapped for you, so you don’t have to assemble raw bytes and craft low-level commands by hand.

However, it’s still essential to understand the general flow:

  1. Initialize NFCTagReaderSession with polling options and assign a delegate.
  2. Call session.begin().
  3. Once a card is detected, the session invokes the corresponding delegate method.
  4. Implement the reading logic inside func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]).
    1. Call requestService inside this method.
    2. Verify the service response, then call readWithoutEncryption.

NFCTagReaderSession

To enable a class to read NFC, you must conform to NFCTagReaderSessionDelegate:

public protocol NFCTagReaderSessionDelegate : NSObjectProtocol {
    // 當 session 可以開始讀取 NFC Tag 時呼叫
    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession)

    // 當呼叫 session invalidate 時呼叫
    func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error)

  	// 當 session 偵測到 tag 時呼叫
    func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
}

On Apple Developer, you can see all available tag types, including popular options like MiFare and FeliCa.

Block List

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

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

Here, 10 means requesting 10 blocks.

FeliCa Reader

To read FeliCa data, you must first know the corresponding service code—such as the service code for ride history or card balance. On this website (Japanese), we can find:

When System Code is 0003:

  • Service code: 008B (1 block) - Card type and balance
  • Service code: 090F (20 blocks) - Ride history (20 entries)
  • Service code: 108F (3 blocks) - Transfer history (3 entries)

For me, ride history and balance are the most interesting pieces of data. Let’s inspect what information is stored inside these blocks:

One block is 16 bytes, and each byte stores corresponding data. Based on the documentation from that site, here are several key fields (numbers indicate byte offsets, and parentheses denote length in bytes):

  • 0 (1 byte): Device type
  • 1 (1 byte): Usage type (fare adjustment, new purchase, auto-reload, etc.)
  • 2 (1 byte): Payment method (credit card, mobile, etc.)
  • 3 (1 byte): Entry/exit type (entry, exit, etc.)
  • 4–5 (2 bytes): Date (Year 7 bits, Month 4 bits, Day 5 bits). The year uses 2000 as the base, so 19 represents 2019.
  • 6–9 (4 bytes): Entry/exit station codes, vending machine info
  • A–B (2 bytes): Balance (Little Endian)
  • C: Unknown
  • F: Region code (Kanto private railways, Chubu private railways, Okinawa private railways, etc.)

With this knowledge, we can parse the information inside a for data in dataList loop! For example, parsing the entry/exit date:

let year = data[4] >> 1 // 不知道為什麼要右移一個才是正確的
let month = UInt(bytes: data[4...5]) >> 5 & 0b1111 // 取得 month 的 bit
let date = data[5] & 0b11111 // 取得 date 的 bit

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

Demo

The source code is available on GitHub.

Wrap-up

Although I successfully read the data, some values weren’t quite what I expected. For instance, I searched extensively online for station codes but still couldn’t find a complete mapping, so I’m not certain if the parsed station codes are accurate.

Additionally, while up to 20 history entries can theoretically be retrieved, reading that many caused failures on certain cards; a safer limit is 10 (still testing this).

Looking at the records on my commuter pass, it also seems that not every tap in/out is logged. If your goal is to log every single journey for visualization or analytics, relying solely on card data might fall short.

Regarding FeliCa reading on iOS, a Japanese developer has built a very handy library called TRETJapanNFCReader. If you’re looking for a ready-to-use solution, definitely check it out. Besides SUICA, it supports many other types of IC cards and can even read driver’s licenses.

Ever since CoreNFC was introduced, I wanted to try reading FeliCa data. However, there weren’t many Chinese resources available. Japanese implementations were plentiful, but terms like service codes and block lists were still puzzling at first. So I spent a day reading through the FeliCa specs and built this implementation.

Also, type casting in Swift can be somewhat cumbersome—such as converting Data to UInt. I found this snippet online, and while I didn’t fully understand the mechanics behind it XD, it allowed me to convert data using UInt(bytes: Data):

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 haven’t been learning iOS development and Swift for very long, so I’m likely unfamiliar with idiomatic patterns and conventions, making the code a bit unpolished.

Related Posts

Explore Other Topics