CoreNFC(FeliCa)を使ってSuicaの情報を読み取る方法
はじめに
iOS 11.0ではCoreNFCを通じてNFCタグの読み取りと書き込みができるようになったが、交通系ICカードなどの情報は読み取れなかった。iOS端末でICカードを読み取れるようになったのはiOS 13以降のことだ。
僕はずっとNFCに興味を持っていて、Suica(日本の交通系ICカード)の情報を自分で読み取って、スマホで直接残高を確認できるようにしたいと思っていた(すでにそうしたアプリがいくつかあるのは知っているが、学習目的で自分で作ってみたかった)。
ネット上にはSuicaに関する中国語や英語のリソースが比較的少ないため、僕は数日かけてFeliCaの仕様書を調査し、CoreNFCを使って実装してみた。
本記事では、まずNFCプロトコルから始め、日本の交通系ICカードで広く使われているFeliCaについて触れ、最後にSwiftでの実装について解説する。
Swiftを学び始めたばかりなので、至らない点があるかもしれないが、気軽にTwitterでメンションしてほしい。
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は本当に高速だ。日本の公共交通機関を利用したことがあれば、改札を通るときに立ち止まる必要がないことに気づくはずだ。
現在広く使われているSuica、ICOCA、はやかけんなどの交通系ICカードは、すべてこのFeliCa技術を採用している。
交通機関だけでなく、コンビニでの決済などにも交通系ICカードを利用できる。
データの安全性
NFCでは、読み取り可能なデータとそうでないデータがあり、一部のカードではデータの読み書きに正しい鍵での復号が必要になる。
現在、AndroidやiOSでNFCカードを読み取れるようになったとはいえ、残高などのデータを書き換えるには正規の暗号鍵が必要だ。
FeliCaのアーキテクチャ
FeliCaのアーキテクチャは、大きくプライベート領域(プ ラ イ ベ ー ト)と共通領域の2つに分けられる。
共通領域には誰でも読み取れる情報が格納され、プライベート領域には個人情報や残高管理などの情報が格納されており、操作には暗号化・復号が必要となる。
共通領域は以下のいくつかの要素に分かれている:
- システム(System):カードの単位を示す。各カードはシステムコード(2バイト)を持つ。JIS X 6319-4の規定によれば、この値はFE00〜AA00の間となる。Suicaのようなカードは
0003だ(この値は重要で、後ほど使用する)。 - エリア(Area):記憶領域や各サービスが格納するブロック数などの情報を含む。
- サービス(Service):外部アクセスのためにデータをブロック単位で保存する。各サービスにはサービスコード(2バイト)があり、例えば入場記録のサービスコードは
090fだ。サービスはランダムサービス、サイクリックサービス、パースサービスに分類される。 - ブロック(Block):データが実際に格納される場所で、1ブロックは16バイト。必要なブロック数はサービスによって異なる。
カード内の情報を読み取るだけであれば、主に「サービス」と「ブロック」の2つを扱うことになる。
サービスの種類
前述の通り、サービスはランダム、サイクリック、パースに分かれており、主にデータアクセスの特性によって区別される。
- ランダムサービス:自由に読み書きできるデータ領域。ベンダーによって定義される。
- サイクリックサービス:履歴データなどの記録を蓄積できる領域。
- パースサービス:残高管理や減額などを管理するための領域。
コマンド
[FeliCa](http://www.proxmark.org/files/Documents/13.56 MHz - Felica/card_usersmanual_2.0.pdf)には多くの種類のコマンドが存在し、[FeliCaの仕様書](http://www.proxmark.org/files/Documents/13.56 MHz - Felica/card_usersmanual_2.0.pdf)で確認できる。FeliCaのデータを読み取るには、以下のいくつかのコマンドが必要となる:
- Polling(ポーリング)
- Request Service(サービス要求)
- Read Without Encryption(暗号化なし読み取り)
各コマンドには対応するリクエストパケットがあり、リクエスト内に何を含めるべきかが規定されている。この部分についてはすでにCoreNFCに対応する関数が用意されているため、心配する必要はない。
FeliCaにおける通信の流れ
- Pollingコマンドでカードを捕捉する。
- Request Serviceコマンドを使い、サービスコードのリストを渡す。カード側でサービスコードが正しいか、読み取り可能かを確認する。サービスが存在しないかエラーの場合は
0xFFFFが返る。 - Read Without Encryptionコマンドを使い、サービスコードを渡して対応するデータをブロック単位で取得する(最大16サービス)。
- このコマンドは status1 と status2 を返す。両方とも0であれば問題ないことを示す。
- 0以外の場合はエラーを意味する。エラーステータスの詳細はFeliCaの仕様書で確認できる。
iOSでの実装
NFCタグの読み取り方法についてはApple Developerのサンプルコードを参照できる。ここではFeliCaTagの読み取り方法に絞って説明する。
NFCを実装するには、まずiPhoneの実機が必要だ。シミュレータではNFCを読み取る手段がない。
デバイスがNFCデータを読み取り可能かどうかは、NFCTagReaderSession.isReadingAvailableで判定できる。
5]) >> 5 & 0b1111 // get month bit
let date = data[5] & 0b11111 // get 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)")!
デモ
ソースコードはGitHubで確認できる。
所感
無事に情報は読み取れたものの、いくつかの数値は僕が想定していたものとは異なっていた。
例えば、駅コードについてはネットで長時間検索しても見つからなかった。そもそも駅コードを正しくパースできているのかすら分かっていない。
履歴は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))
}
}
}
ここまで読み進めるのは長い道のりだったと思う。NFCがどのように動作するのか、FeliCaとは何なのか、そして何よりSwiftでCoreNFCと組み合わせてどう使うのかを理解してもらえたら嬉しい!
関連記事
- 測定が目標になるとき:窓税から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万件のデータで検証したベンチマークと設計上の意思決定を解説する。