If you have any questions or feedback, pleasefill out this form
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
Introduction
Starting from iOS 11.0, you can read and write NFC Tags using CoreNFC, but it wasn't until iOS 13 that support for reading IC cards was introduced.
I have always been interested in NFC technology and wanted to read the information from my Suica card (a Japanese transportation IC card) so that I could check my balance directly on my phone (I know there are apps that already do this). However, there are relatively few Chinese resources online regarding reading Suica cards, so I spent a few days studying the FeliCa documentation and implementing it with CoreNFC.
This article will start with an overview of the NFC protocol, then discuss the widely-used FeliCa technology in Japanese transportation IC cards, and finally cover the implementation in Swift. Please note that I am still a beginner with Swift, so there may be some areas that are not well written.
What is NFC?
NFC (Near Field Communication) is a protocol for short-range communication and also a type of RFID wireless communication technology.
This protocol mainly specifies:
- Communication Protocol: How the transmitter and receiver communicate with each other
- Data Exchange: How the two parties exchange data
This article will focus on the process of reading data from FeliCa.
Problems Solved by NFC
In wireless communication, we can use technologies like Bluetooth or Wi-Fi to communicate, but the biggest issues are security and pairing.
With Bluetooth, you often need to pair devices before they can communicate, which can be cumbersome. If a reader can detect card information from 20 meters away or directly request payment, there are significant security concerns.
That's why NFC typically operates at a distance of just a few centimeters, ensuring safety and reducing interference.
FeliCa
FeliCa is a contactless IC card technology developed by Sony in 2001. Compared to NFC Type-A and Type-B, FeliCa is faster, likely due to the heavy commuter traffic in Japan.
By the way, Taiwan's EasyCard is an IC card manufactured using Philips' Mifare technology, which is currently one of the most widely used contactless IC cards globally. It seems that only Japan extensively uses FeliCa.
FeliCa is incredibly fast; if you've used public transportation in Japan, you might have noticed that you don't even need to stop when entering the platform. Popular transportation IC cards like Suica, ICOCA, and はやかけん utilize FeliCa technology. Besides transportation, convenience stores often allow payments via these IC cards.
Data Security
In NFC, some data can be read while other data cannot, and some cards require the correct keys for decryption before data can be read or written.
Although both Android and iOS can read NFC cards now, tampering with balances or similar data requires the correct keys.
FeliCa Architecture
In the FeliCa architecture, there are two main categories: Private Domain (プライベート領域) and Common Domain (共通領域).
The Common Domain contains information that can be read, while the Private Domain holds sensitive information such as personal data or balance control, which can only be accessed through authentication.
The Common Domain can be further divided into several parts:
- System: Represents the unit of the card. Each card has a system code (2 bytes). According to Japan's JIS 6319-4 standards, this value ranges from FE00 to AA00. For instance, Suica's system code is
0003
(this value is important and will come up later). - Area: Contains information such as storage capacity and the number of blocks allocated 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/exit records is
090f
. Services are further categorized into random service, cyclic service, and pass service. - Block: The storage location for data. Each block is 16 bytes, and the number of blocks required varies by service.
When simply reading information, you'll primarily deal with the service and block components.
Types of Services
As mentioned earlier, services are categorized into random, cyclic, and pass, primarily differentiated by data access methods.
- Random Service: Allows free read/write access to data determined by the manufacturer.
- Cyclic Service: Records historical data.
- Pass Service: Manages balances, deposits, and withdrawals.
Commands
In FeliCa, there are many commands available, which can be found in the FeliCa documentation. To read FeliCa data, you will need a few commands:
- polling
- request service
- Read without encryption
Each command has a corresponding request packet that specifies the expected contents of the request. CoreNFC provides functions for these commands.
Communicating with FeliCa
- Use the polling command to capture the card.
- Use the Request Service command, passing in the service code list. The card will confirm whether the service code is correct or accessible. If the service does not exist or is incorrect, it will return
0xFFFF
. - Use the Read without Encryption command, passing in the service code, which will return the corresponding data in blocks (up to 16 services).
- This command will return status1 and status2; if both are 0, there are no issues.
- If either is not 0, an error occurred, and there are many potential error codes that won't all be listed here.
iOS Implementation
For information on how to read NFC Tags, you can refer to the example on Apple Developer, but 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 whether NFC reading is available using NFCTagReaderSession.isReadingAvailable
.
First, refer to the CoreNFC documentation to add the corresponding entries to your info.plist and entitlement.
1. Add Near Field Communication Tag Reading
Capability
2. Add ISO18092 system codes for NFC Tag Reader Session in Info.plist
- Corresponding to the previous system code, if you want to read cards like Suica, it is
0003
. - Add Privacy - NFC Scan Usage Description.
CoreNFC Implementation
In NFCTagReaderSession, the polling, requestService, and readWithoutEncryption commands are already encapsulated in functions, so you don't need to manually input a bunch of numbers and understand the commands.
However, you still need to understand the general flow:
- Create an
NFCTagReaderSession
and set the pollingOption and delegate. - Call
session.begin()
. - When a card is detected, the session will call the corresponding delegate method.
- Implement the reading logic in the
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
function.- Here, call requestService.
- After confirming the service response, call readWithoutEncryption.
NFCTagReaderSession
To enable a class to read NFC, you must implement NFCTagReaderSessionDelegate
.
public protocol NFCTagReaderSessionDelegate : NSObjectProtocol {
// Called when the session is ready to start reading 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
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
}
You can find the available tag types on Apple Developer, including mainstream types like MiFare and FeliCa.
Block List
The block list refers to which parts of the service blocks to read. When using readWithoutEncryption
, you need to pass the blockList as a parameter. You can refer to the documentation for the definition of each bit in the blockList. In CoreNFC, you can write it like this:
let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) }
Here, 10 means returning 10 blocks.
FeliCa Reader
To read FeliCa data, you first need to know the corresponding service codes for the ride history or card balance. You can find this information on this website (in Japanese):
When the System Code is 0003:
- Service Code: 008B (1 block) records card type and balance
- Service Code: 090F (20 blocks) ride history records (20 entries)
- Service Code: 108F (3 blocks) transfer history records (3 entries)
I am particularly interested in the ride history and balance. Let’s check what information is recorded in this block:
Each block contains 16 bytes, with each byte storing corresponding data. Here are a few items based on the website's documentation, with numbers representing the byte index, and parentheses showing how many bytes each piece of data occupies:
- 0 (1 byte): Machine type
- 1 (1 byte): Usage type (settlement, new, auto-recharge, 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 a base, so 19 represents 2019.
- 6 ~ 9 (4 bytes): Entry/exit station code, vending machine information
- A ~ B (2 bytes): Balance (Little Endian)
- C: Unknown
- F: Local code (e.g., Kanto private rail, Chubu private rail, Okinawa private rail, etc.)
With this information, we can read the data in for data in dataList
! For example, to get the entry/exit date:
let year = data[4] >> 1 // Not sure why it needs to be right-shifted by one
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.
Conclusion
Although I successfully read the information, some numbers didn't match my expectations. For example, I couldn't find the station codes online after searching for a long time, and I'm uncertain whether the parsed station codes are correct.
Moreover, while I can retrieve 20 historical records, some cards may fail to read at that capacity; a safer limit seems to be 10 (still testing).
Looking at the records (my card is a commuter pass), it appears not every entry and exit is recorded. If you're looking to collect your entry and exit data using the card, it may not be very practical (e.g., for visualization).
Regarding FeliCa reading, Japanese developers have already created a fairly useful library for iOS called TRETJapanNFCReader. You can check it out if you want a more straightforward implementation; it supports various types of IC cards beyond just SUICA, even including driver's licenses.
When I first discovered the CoreNFC SDK, I wanted to try reading FeliCa information. However, there weren't many relevant resources in Chinese online; there are quite a few implementations in Japanese, but I still struggled to understand things like service codes and block lists. I spent a day going through the FeliCa documentation to implement it.
Additionally, converting types in Swift can be quite cumbersome, like converting Data to UInt. I found this code online, but I didn't fully understand what it was doing XD; I just know I can convert 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 long, so I might not be familiar with some common practices or development methods, which may make my writing a bit disorganized.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee