· 8分で読了

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における通信の流れ

  1. Pollingコマンドでカードを捕捉する。
  2. Request Serviceコマンドを使い、サービスコードのリストを渡す。カード側でサービスコードが正しいか、読み取り可能かを確認する。サービスが存在しないかエラーの場合は0xFFFFが返る。
  3. Read Without Encryptionコマンドを使い、サービスコードを渡して対応するデータをブロック単位で取得する(最大16サービス)。
    1. このコマンドは status1 と status2 を返す。両方とも0であれば問題ないことを示す。
    2. 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と組み合わせてどう使うのかを理解してもらえたら嬉しい!

関連記事

他のトピックを探索