iOS 11.0 開始可以透過 CoreNFC 來讀寫 NFC Tag,不過還無法讀取 IC 卡片之類的資訊,到了 iOS 13 後才開放 IC 卡讀取。
剛好一直以來對 NFC 蠻有興趣的,而且也想要自己讀取 Suica (日本交通 IC 卡)的資訊,以後就可以直接用手機看餘額了(我知道已經有 app 做到這件事了)不過在網路上關於 Suica 讀取的中文資源比較少,所以花了幾天研究了一下 FeliCa 文件,並且用 CoreNFC 實作。
這篇文章會先從 NFC 協定開始講起,再談到日本交通 IC 卡中廣泛使用的 FeliCa,最後是 swift 中的實作。不過我的 swift 也才剛起步,可能會有寫得不是很好的地方。
NFC(Near Field Communication)是個近距離溝通的協定,同時也是 RFID 無線通信技術。
這個協定主要規範了:
本篇內容會限縮在 FeliCa 的資料讀取過程。
在無線通信上,我們可以利用藍牙、wifi 等等來做溝通,但最大的問題在於安全性以及配對。
在藍牙上可能需要先藍芽配對後才能彼此溝通,設定上比較繁瑣一些,如果讀取機可以在 20 公尺遠的地方就能夠偵測卡片資訊,或者是直接要求付款,在安全性上就有很大的問題。
因此在 NFC 當中,偵測距離通常都在幾公分之內,除了確保安全性之外,也可以減少雜訊干擾。
Felica 是由 Sony 在 2001 年開發的非接觸式 IC 卡技術。跟 NFC Type-A 與 Type-B 比起來更加快速,原因可能是日本可怕的通勤人潮吧。
順帶一提,台灣的悠遊卡是由飛利浦公司設計的 Mifare 製造的 IC 卡,同時 Mifare 也是目前全球廣泛使用的非接觸式 IC 卡片,目前似乎只有日本廣泛使用 FeliCa。
FeliCa 的速度真的很快,在日本如果有搭乘大眾運輸工具的話應該會發現進入月台完全不用停下來。目前廣泛使用的交通 IC 卡像是 Suica、ICOCA、はやかけん 等等都是採用 FeliCa 技術,除了交通之外,在便利超商付款上也常常利用交通 IC 卡來付款。
在 NFC 當中有些資料可以讀取、有些資料不行,有些卡片需要正確的金鑰解密後才能夠讀寫資料。
雖然說 Android 跟 iOS 現在都已經可以讀取 NFC 卡片了,但如果要篡改餘額之類的資料,必須要有正確的金鑰才行。
在 FeliCa 的架構中,主要可以分為兩大類:私有領域(プライベート領域)跟共通領域。
共通領域存放一些可供讀取的資訊,而私有領域則存放一些像是個人資料或控制餘額等的訊息,必須要透過加解密認證才能夠操作。
而在共通領域又可以分為幾個部分:
0003
(這個值很重要,等下會出現)090f
。service 又分為 random service、cyclic service、pass service單純讀取資訊的話,比較會碰到的是 service 與 block 這兩部分。
前面有提到 service 又分為 random、cyclic、pass,主要是以 data access 的方式區分。
在 FeliCa 當中有很多種指令,可以在 [FeliCa 的文件](http://www.proxmark.org/files/Documents/13.56 MHz - Felica/card_usersmanual_2.0.pdf)當中找到,要讀取 FeliCa 資料的話需要幾個指令:
每個指令都有一個對應的 request pocket,規範了 request 當中應該要有哪些內容,這部分在 CoreNFC 中已經提供好對應的函數可供使用。
0xFFFF
關於如何讀取 NFC Tag,可以參考 apple developer 上的範例,這邊主要會專注在如何讀取 FeliCaTag
要實作 NFC,首先必須要有一台實體的 iPhone,在模擬器上並沒有辦法讀取 NFC。
能不能讀取 NFC,可以透過 NFCTagReaderSession.isReadingAvailable
來判斷。
首先參考 CoreNFC 的文件,加入對應的 info.plist 還有 entitlement。
Near Field Communication Tag Reading
在 NFCTagReaderSession 當中,polling、requestService、readWithoutEncryption 都已經有對應的函數包好了,不需要辛辛苦苦塞一堆數字跟理解 command。
不過大致上的流程還是要理解一下:
建立 NFCTagReadersession
並且設定 pollingOption 跟 delegate
呼叫 session.begin()
在讀取到卡片後 session 會呼叫對應的 delegate 方法
在
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
函數中實作讀取邏輯。
要讓 class 可以具有讀取 NFC 的功能,首先要實作 NFCTagReaderSessionDelegate
public protocol NFCTagReaderSessionDelegate : NSObjectProtocol {
// 當 session 可以開始讀取 NFC Tag 時呼叫
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession)
// 當呼叫 session invalidate 時呼叫
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error)
// 當 session 偵測到 tag 時呼叫
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
}
在 apple developer 上可以找到目前有的 tag type,主流的 MiFare 或是 FeliCa 都有。
block list 是指要讀取 service 中 block 的哪些部分,在 readWithoutEncryption
的時候需要 blockList 當作參數,關於 blockList 中每個 bit 的定義,可以參考文件上的說明,在 CoreNFC 中你可以這樣子寫:
let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) }
這邊的 10 代表回傳 10 個 block。
要讀取 FeliCa 的資料,首先要先知道對應的 service code 是多少,乘車歷史的 service code 或是卡片餘額的 service code。在這個網站(日文)中可以找到:
System Code 是 0003 的情況下:
其中對我來說乘車歷史紀錄跟餘額應該是最感興趣的資料,來看看這個 block 的內容記錄了哪些資訊:
一個 block 有 16 byte,每個 byte 都中都存放著對應的資料,以下根據網站中的文件列出幾個,數字代表第幾個 byte,括號代表這個資料的長度佔用幾個 byte。
知道這些訊息之後,我們就可以在 for data in dataList
讀取資料了!例如出入場的時間可以:
let year = data[4] >> 1 // 不知道為什麼要右移一個才是正確的
let month = UInt(bytes: data[4...5]) >> 5 & 0b1111 // 取得 month 的 bit
let date = data[5] & 0b11111 // 取得 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 上已經有日本開發者寫出一套還蠻好用的 library TRETJapanNFCReader,如果想要直接使用的話可以參考看看,除了 SUICA 之外他還支援了蠻多類型的 IC 卡,甚至連駕照都可以讀取。
當初看到 CoreNFC 這個 SDK 就想要來試試看讀取 FeliCa 資訊了,不過網路上沒有太多相關中文資源,日文的實作倒是蠻多的,只是看到 service code,blockList 什麼的還是不知所以然,所以花了一天的時間看完了 FeliCa 的文件並且試著實作出來。
另外在 swift 的型別轉換上蠻麻煩,像是把 Data 轉換成 UInt 等等,從網路上找來了這段 code,但看不太懂他在幹嘛 XD,只知道我可以用 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 的時間還不長,因此可能不知道一些慣用手法或開發方式,寫得有點亂七八糟的。
這篇好有趣!但有個地方看得沒有很明白,FeliCa 是 NFC 底下的一種協定嗎?因為 Android 手機基本上要明確支援 FeliCa 才能使用相關功能(如 mobile suica),像我的手機雖然有支援 NFC,但因為沒有支援 FeliCa(日本販賣的才有),所以就沒辦法用,不知道是軟體限制還是硬體的限制