CoreNFC を使ってスイカの情報を読み取る方法

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

前言

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 と比較しても、より高速です。その理由は、日本の通勤ラッシュの厳しさかもしれません。

余談ですが、台湾の悠遊卡はフィリップス社が設計した Mifare を使用した IC カードで、Mifare は現在、世界中で広く使用されている非接触式 IC カードです。しかし、FeliCa は日本でのみ広く使用されています。

FeliCa の速度は本当に速く、日本では公共交通機関を利用する際、プラットフォームに入るときに全く止まることなく通過することができるでしょう。現在広く使用されている交通 IC カード、例えば Suica、ICOCA、はやかけん などは、すべて FeliCa 技術を採用しています。交通機関以外でも、コンビニでの支払いに交通 IC カードを利用することがよくあります。

データの安全性

NFC では、読み取れるデータと読み取れないデータがあり、一部のカードでは正しい鍵が必要でなければデータの読み書きができません。

現在、Android と iOS の両方が NFC カードを読み取ることができるようになりましたが、残高などのデータを改ざんするには正しい鍵が必要です。

FeliCa 構造

FeliCa の構造は、主に二つの大きなカテゴリに分けられます:プライベート領域と共通領域です。

共通領域には読み取り可能な情報が保存されており、プライベート領域には個人情報や残高管理などの情報が保存されています。これらは暗号化認証を通じて操作する必要があります。

共通領域はさらにいくつかの部分に分かれています:

  • System:カードの単位を示します。各カードには system code(2 バイト)があり、日本の JIS 6319-4 によって、FE00 から AA00 の範囲にあることが規定されています。Suica のようなカードの場合は 0003 です(この値は非常に重要ですので、後で登場します)。
  • Area:ストレージや各サービスのブロック数などの情報が含まれています。
  • Service:データをブロック単位で保存し、外部からアクセスできるようにします。各サービスには service code(2 バイト)があり、例えば出入場記録の service code は 090f です。service は random service、cyclic service、pass service に分かれます。
  • Block:データの保存場所で、各ブロックは 16 バイトであり、サービスによって必要なブロック数は異なります。

単純に情報を読み取る場合、主に service と block の部分に関連します。

Service の種類

前述したように、service は random、cyclic、pass に分かれ、主にデータアクセスの方法で区別されます。

  • Random Service:自由に読み書きできる、製造者が決定したデータ。
  • Cyclic Service:歴史データを記録する場所。
  • Pass Service:残高や入金・出金を管理する場所。

コマンド Command

FeliCa には多くの指令があり、FeliCa のドキュメントで確認できます。FeliCa のデータを読み取るためには、いくつかの指令が必要です:

  • polling
  • request service
  • Read without encryption

各指令には対応する request pocket があり、要求に含まれるべき内容を規定しています。この部分は CoreNFC で既に対応する関数が用意されています。

FeliCa と通信するプロセス

  1. polling command を通じてカードを捕捉します。
  2. Request Service 指令を使用し、service code リストを渡します。カードは service code が正しいか、読み取れるかを確認します。サービスが存在しないか間違っている場合は 0xFFFF が返されます。
  3. read without encryption 指令を使用し、service code を渡すと、対応するデータが(ブロック単位で)返されます。(サービスは最大 16 個まで)
    1. この指令は status1 と status2 を返します。どちらも 0 の場合は問題ありません。
    2. 0 以外の場合はエラーを示し、エラーコードは多くの状態があるため、ここでは全て列挙しません。

iOS 実装

NFC タグの読み取り方法については、Apple Developer の サンプル を参照できますが、ここでは FeliCaTag の読み取りに焦点を当てます。

NFC を実装するには、まず実物の iPhone が必要で、シミュレーターでは NFC を読み取ることはできません。

NFC の読み取り可否は NFCTagReaderSession.isReadingAvailable で確認できます。

まず CoreNFC のドキュメント を参照し、対応する info.plist とエンタイトルメントを追加します。

1. Capabilities に Near Field Communication Tag Reading を追加

ios-capability

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

plist

  • 先ほどの system code に対応し、Suica のようなカードを読み取る場合は 0003 です。
  • Privacy - NFC Scan Usage Description を追加します。

CoreNFC 実装

NFCTagReaderSession では、polling、requestService、readWithoutEncryption についてすでに対応する関数が用意されており、数字を大量に詰め込んだりコマンドを理解する必要はありません。

ただし、大まかな流れは理解する必要があります:

  1. NFCTagReaderSession を作成し、pollingOption と delegate を設定します。
  2. session.begin() を呼び出します。
  3. カードが読み取られると、session は対応する delegate メソッドを呼び出します。
  4. func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) 関数内で読み取りロジックを実装します。
    1. ここで requestService を呼び出します。
    2. サービスの応答を確認後、readWithoutEncryption を呼び出します。

NFCTagReaderSession

クラスが NFC の読み取り機能を持つためには、まず NFCTagReaderSessionDelegate を実装する必要があります。

public protocol NFCTagReaderSessionDelegate : NSObjectProtocol {
    // セッションが NFC タグの読み取りを開始できるようになったときに呼び出される
    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession)

    // セッションが無効化されたときに呼び出される
    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 リーダー

FeliCa のデータを読み取るには、まず対応する service code を知っておく必要があります。乗車履歴の service code やカード残高の service code を確認するためには、このウェブサイト(日文)を参照できます。

System Code が 0003 の場合:

  • service code: 008B (1 block) カードの種類と残高を記録
  • service code: 090F(20 block)乗車履歴(20件)
  • service code: 108F(3 block)途中乗換の履歴(3件)

私にとって乗車履歴と残高は最も興味深いデータであり、このブロックの内容にはどのような情報が記録されているのか見てみましょう:

1つのブロックは 16 バイトであり、各バイトには対応するデータが保存されています。以下は、サイトのドキュメントに基づくいくつかの情報で、数字はバイトの位置、括弧はデータの長さを示しています。

  • 0 (1 byte):機器の種類
  • 1 (1 byte):使用の種類(精算、新規、自動チャージなど)
  • 2 (1 byte):支払い方法(クレジットカード、モバイルなど)
  • 3 (1 byte):出入場の種類(入場、出場など)
  • 4 ~ 5 (2 byte):年月日(年 7 bit、月 4 bit、日 5 bit)、年は 2000 年を基準とし、19 は 2019 年を表します。
  • 6 ~ 9 (4 byte):出入場の駅コード、販売機情報
  • A ~ B (2 byte):残高(Little Endian)
  • C:不明
  • F:地域コード(関東私鉄、中部私鉄、沖縄私鉄など)

これらの情報を知った後、for data in dataList でデータを読み取ることができます。例えば、出入場の時間を取得するには:

let year = data[4] >> 1 // なぜ右にシフトする必要があるのかは不明
let month = UInt(bytes: data[4...5]) >> 5 & 0b1111 // month のビットを取得
let date = data[5] & 0b11111 // date のビットを取得

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 の情報を読み取ってみたいと思ったのですが、ネット上にはあまり関連する中国語のリソースがなく、逆に日本語の実装が多く見受けられました。ただ、service code や 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 の学習を始めてそれほど時間が経っていないため、慣用手法や開発スタイルがまだわからず、少し混乱した内容になってしまいました。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee