CoreNFCを使ってSuicaの情報(FeliCa)を読み取る方法
はじめに
iOS 11.0からCoreNFCを通じてNFCタグの読み書きができるようになったが、まだICカードなどの情報は読み取れず、iOS 13になってようやくICカードの読み取りが開放された。
ちょうど以前からずっとNFCに興味があり、自分でSuica(日本の交通系ICカード)の情報を読み取って、スマホで直接残高を確認できるようにしたいと思っていた(すでにそれを実現しているアプリがあるのは知っているが)。しかしネット上にはSuicaの読み取りに関する中国語のリソースが少なかったため、数日かけてFeliCaの仕様書を調べ、CoreNFCで実装してみた。
この記事では、まずNFCプロトコルから始め、日本の交通系ICカードで広く使われているFeliCaについて触れ、最後にSwiftでの実装を解説する。ただ、僕のSwift歴もまだ浅いため、拙いコードがあるかもしれない。
NFCとは何か?
NFC(Near Field Communication)は近距離通信のプロトコルであり、RFID無線通信技術の一種でもある。
このプロトコルは主に以下を定義している:
- 通信プロトコル:送信側と受信側がどのように通信するか
- データ交換:両者がどのようにデータを交換するか
本記事の内容は、FeliCaのデータ読み取りプロセスに焦点を絞って解説する。
NFCが解決する課題
無線通信では、BluetoothやWi-Fiなどを利用して通信できるが、最大の問題はセキュリティとペアリングにある。
Bluetoothでは通信前にお互いをペアリングする必要があり、設定がやや煩雑だ。また、もし読み取り機が20メートル離れた場所からカード情報を検出できたり、直接支払いを要求できたりしたら、セキュリティ上大きな問題になる。
そのため、NFCにおける検出距離は通常数センチ以内に制限されており、安全性を確保するとともに、ノイズの干渉も減らしている。
FeliCa
FeliCaは、ソニーが2001年に開発した非接触ICカード技術だ。NFC Type-AやType-Bと比較して処理速度が非常に高速だが、その背景には日本のすさまじい通勤ラッシュがあるのかもしれない。
ちなみに、台湾の悠遊カード(EasyCard)はフィリップス社が設計したMifare製のICカードだ。Mifareは現在世界中で広く使われている非接触ICカードだが、FeliCaが広く普及しているのは現在のところ日本だけのようだ。
FeliCaの速度は本当に速く、日本で公共交通機関を利用したことがあれば、改札を通る際に立ち止まる必要がまったくないことに気づくだろう。現在広く使われている交通系ICカード(Suica、ICOCA、はやかけんなど)はすべてFeliCa技術を採用しており、交通機関だけでなくコンビニなどでの支払いにも広く利用されている。
データの安全性
NFCの中には読み取り可能なデータとそうでないデータがあり、一部のカードでは正しい暗号鍵で復号しないとデータの読み書きができない。
AndroidやiOSは現在NFCカードの読み取りに対応しているが、残高などのデータを改ざんするには正しい暗号鍵が必要となる。
FeliCaの構造
FeliCaの構造は、主に「プライベート領域」と「共通領域」の2つに分けることができる。
共通領域には自由に読み取れる情報が格納され、プライベート領域には個人情報や残高を制御する情報など、暗号化と認証を経て初めて操作できる情報が格納されている。
そして共通領域はさらにいくつかの要素に分かれている:
- システム(System):カードの単位を表す。各カードにはシステムコード(System Code、2バイト)があり、日本のJIS X 6319-4の規定によると、この値はFE00〜AA00の間になる。Suicaなどのカードは
0003だ(この値は重要で、後ほど登場する)。 - エリア(Area):ストレージ領域や、各サービスが格納するブロック数などの情報を含む。
- サービス(Service):外部からアクセスできるようにデータをブロック形式で保存する。各サービスにはサービスコード(Service Code、2バイト)があり、例えば乗降履歴のサービスコードは
090fだ。サービスはさらにランダムサービス、サイクリックサービス、パスサービスに分かれる。 - ブロック(Block):データが実際に格納される場所で、1ブロックは16バイト。サービスによって必要なブロック数が異なる。
単に情報を読み取るだけであれば、主に関わるのはサービスとブロックの2つだ。
サービスの種類
先ほどサービスにはランダム、サイクリック、パスの3種類があると述べたが、これらは主にデータのアクセス方法によって分類されている。
- ランダムサービス(Random Service):自由に読み書き可能で、製造元によって決定されるデータ。
- サイクリックサービス(Cyclic Service):履歴データなどを記録する場所。
- パスサービス(Pass Service):残高やチャージ・引き落としなどを管理する場所。
コマンド(Command)
FeliCaには様々なコマンドが存在し、FeliCaの仕様書で確認できる。FeliCaのデータを読み取るには、以下のいくつかのコマンドが必要になる:
- Polling(ポーリング)
- Request Service(リクエストサービス)
- Read Without Encryption(非暗号化読み取り)
各コマンドには対応するリクエストパケットがあり、リクエストに含めるべき内容が規定されている。この部分についてはCoreNFC側で対応する関数が用意されている。
FeliCaとの通信フロー
- Pollingコマンドを使ってカードを補足(検出)する。
- Request Serviceコマンドを使い、サービスコードリストを渡す。カードはサービスコードが正しいか、読み取り可能かを確認する。サービスが存在しないか誤っている場合は
0xFFFFが返される。 - Read Without Encryptionコマンドを使い、サービスコードを渡すと、対応するデータがブロック単位で返される(指定できるサービスは最大16個)。
- このコマンドはstatus1とstatus2を返し、両方とも0であれば問題ないことを示す。
- 0でない場合はエラーを意味する。エラーコードの状態は多岐にわたるため、ここでは割愛する。
iOSでの実装
NFCタグの読み取り方法については、Apple Developerのサンプルコードを参考にできる。ここでは主にFeliCaTagの読み取り方法に焦点を当てる。
NFCを実装するには、まず実機のiPhoneが必要だ。シミュレータではNFCの読み取りができない。
NFCが読み取り可能かどうかは、NFCTagReaderSession.isReadingAvailableで判定できる。
まずはCoreNFCのドキュメントを参考に、適切なInfo.plistとEntitlementを追加する。
1. Capabilitiesに Near Field Communication Tag Reading を追加

2. Info.plistに「ISO18092 system codes for NFC Tag Reader Session」を追加

- 先ほどのシステムコードに対応しており、Suicaなどのカードを読み取る場合は
0003を指定する。 Privacy - NFC Scan Usage Descriptionも追加する。
CoreNFCでの実装
NFCTagReaderSessionでは、Polling、requestService、readWithoutEncryptionに対応する関数がすでにラップされているため、苦労して数値を詰め込んだり低レベルなコマンドを理解したりする必要はない。
ただし、全体的なフローは理解しておく必要がある:
NFCTagReaderSessionを作成し、pollingOptionとdelegateを設定する。session.begin()を呼び出す。- カードを検出すると、sessionが対応するdelegateメソッドを呼び出す。
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])関数内で読み取りロジックを実装する。- この中でrequestServiceを呼び出す。
- サービスのレスポンスを確認した後、readWithoutEncryptionを呼び出す。
NFCTagReaderSession
クラスにNFC読み取り機能を持たせるには、まずNFCTagReaderSessionDelegateを実装する。
public protocol NFCTagReaderSessionDelegate : NSObjectProtocol {
// セッションがNFCタグの読み取りを開始できる状態になったときに呼ばれる
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession)
// セッションが無効化(invalidate)されたときに呼ばれる
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error)
// セッションがタグを検出したときに呼ばれる
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
}
Apple Developer上では現在利用可能なタグの種類を確認でき、メジャーなMifareやFeliCaに対応している。
Block List
ブロックリスト(Block List)とは、サービス内のブロックのどの部分を読み取るかを指定するものだ。readWithoutEncryptionを実行する際に引数としてblockListが必要になる。blockListの各ビットの定義については仕様書を参照してほしいが、CoreNFCでは以下のように書くことができる:
let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) }
ここでの10は、10個のブロックを返すことを意味している。
FeliCa Reader
FeliCaのデータを読み取るには、まず対応するサービスコード(乗車履歴のサービスコードやカード残高のサービスコード)を知る必要がある。こちらのサイトに情報がまとまっている:
システムコードが0003の場合:
- サービスコード:008B(1ブロック)カード種別と残高を記録
- サービスコード:090F(20ブロック)乗車履歴データ(20件)
- サービスコード:108F(3ブロック)途中改札の履歴データ(3件)
僕にとって最も興味深いデータは乗車履歴と残高だ。このブロックの内容にどんな情報が記録されているかを見てみよう:
1つのブロックは16バイトあり、各バイトに対応するデータが格納されている。上記サイトのドキュメントを参考にいくつか列挙してみる。数字は何番目のバイトかを表し、括弧内はそのデータが占めるバイト数を示す。
- 0(1バイト):機器種別
- 1(1バイト):利用種別(精算、新規、オートチャージなど)
- 2(1バイト):決済種別(クレジットカード、モバイルなど)
- 3(1バイト):入出場種別(入場、出場など)
- 4〜5(2バイト):年月日(年7ビット、月4ビット、日5ビット)。年は2000年を基準としているため、19なら2019年を意味する。
- 6〜9(4バイト):入出場の駅コード、自動販売機情報
- A〜B(2バイト):残高(リトルエンディアン)
- C:不明
- F:地域識別コード(関東私鉄、中部私鉄、沖縄私鉄など)
これらの情報がわかれば、for data in dataListでデータを読み取ることができる!例えば入出場の時間は次のように取得できる:
let year = data[4] >> 1 // なぜか1ビット右シフトすると正しい値になる
let month = UInt(bytes: data[4...5]) >> 5 & 0b1111 // 月のビットを取得
let date = data[5] & 0b11111 // 日のビットを取得
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.date(from: "20\(year)-\(month)-\(date)")!
デモ
GitHubでソースコードを公開している。
あとがき
無事に情報を読み取ることはできたが、いくつかの数値は僕が想像していたものと少し違っていた。例えば駅コードはネットで長い時間探したものの見つからず、パースした駅コードが正しいのかどうか分からない。
また、履歴データは仕様上20件取得できるとされているが、一部のカードでは20件取得しようとすると読み取りに失敗することがあり、安全な範囲は10件程度だった(現在も検証中)。
ただ、中身の記録を見てみると(僕のカードは定期券なのだが)、すべての入出場が記録されているわけではないようだ。カードを使って自分の乗降履歴データを収集したい(可視化するなど)と考えているなら、あまり実用的ではないかもしれない。
FeliCaの読み取りに関して、iOSではすでに日本の開発者がTRETJapanNFCReaderという使い勝手の良いライブラリを開発している。直接使いたい場合は参考にしてみると良いだろう。Suica以外にも非常に多くの種類のICカードに対応しており、運転免許証の読み取りまでサポートしている。
そもそもCoreNFCというSDKの存在を知ったときからFeliCaの情報を読み取ってみたいと思っていたが、ネット上には関連する中国語のリソースがあまり多くなかった。日本語の実装例はたくさんあったものの、サービスコードやblockListといった概念がよく分からなかったため、丸一日かけてFeliCaの仕様書を読み込み、なんとか実装してみた次第だ。
また、Swiftにおける型変換は少々面倒で、例えばDataからUIntへの変換などが挙げられる。ネットから以下のコードを拾ってきたのだが、何をやっているのか正確には理解できていない(笑)。ただ、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))
}
}
}
iOS開発やSwiftを勉強し始めてまだ日が浅いため、慣例的な手法や開発作法を知らない部分も多く、コードがやや散らかってしまったかもしれない。
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。