Help us understand the problem. What is going on with this article?

iPhone で運転免許証を読み取ってみよう!【ライブラリを使わずに350行の Swift で本籍を読み取る】

前回の iPhone で運転免許証を読み取ってみよう!【ライブラリを使って】 では Core NFC を用いて開発したライブラリ、treastrain/TRETJapanNFCReader を使って、iPhone で運転免許証を読み取るサンプルを紹介しました。

そして今回はこのライブラリを使わずに、直接 Core NFC を操って運転免許証を読み取ることを目的に記事を書いていきます。

……としましたが、運転免許証の全てを読み取るにはかなりのコード量になるので、とりあえず今の運転免許証には印字されていない「本籍」のみを取得するコードを紹介します。

環境

  • 開発
    • Xcode Version 11.2.1 (11B500)
    • Apple Swift version 5.1.2 (swiftlang-1100.0.278 clang-1100.0.33.9)
    • macOS Catalina 10.15.1(19B88)
  • 実機
    • iPhone 11 Pro (A2215、MWCC2J/A)
    • iOS 13.2.3 (17B111)
  • 仕様
    • 運転免許証及び運転免許証作成システム等仕様書 (仕様書バージョン番号:008) - 警察庁交通局運転免許課

運転免許証(本籍)を読み取るまでの流れ

警察庁交通局運転免許課の運転免許証及び運転免許証作成システム等仕様書 (仕様書バージョン番号:008)に載っている流れ図を引用し、本籍を読み取るまでの流れを確認します。
スクリーンショット 2019-12-07 18.43.55.png
※運転免許証及び運転免許証作成システム等仕様書 (仕様書バージョン番号:008) p.2-1 より引用

本籍は DF1 の EF02 に格納されています。
運転免許証に接続した後、一番上である MF を選択します。その後、暗証番号1を照合し、それが正しい場合はそのまま暗証番号2の照合を行います。それが成功したら DF1 を選択し、続いて EF02 を選択、その場所のバイナリを読み取ります。

本籍以外の情報が格納されている場所、ファイル構成は p.2-1 に掲載されています。

バイナリは BER-TLV フォーマットになっており、今回読み取る本籍は

  • タグ: 最初の1バイト
  • 長さ: その次の1バイト
  • 値: 残りの"長さ"バイト

のようになっています。本籍のバイナリのファイル容量は82バイトです。

Xcode プロジェクトの作成

まずは Create a new Xcode project から iOS の Single View App を作成します。

Capability と Entitlements の設定

Project の Target から Signing & Capabilities を選択し、「+ Capability」から「Near Field Communication Tag Reading」を追加します。
スクリーンショット 2019-11-29 18.50.17.png
前回の記事から流用

すると、「.entitlements」というファイルが追加されているので、「Near Field Communication Tag Reader Session Formats」に「NFC Data Exchange Format」および「NFC tag-specific data protocol」があることを確認します。
スクリーンショット 2019-11-29 19.01.06.png
前回の記事から流用

Info.plist の設定

Info.plist に「Privacy - NFC Scan Usage Description」と「ISO7816 application identifiers for NFC Tag Reader Session」を追加します。
「Privacy - NFC Scan Usage Description」には NFC を何のために使用するのかについての説明を、「ISO7816 application identifiers for NFC Tag Reader Session」の配下には以下の AID を記述します。

  • A0000002310100000000000000000000
  • A0000002310200000000000000000000
  • A0000002480300000000000000000000

スクリーンショット 2019-11-29 20.39.40.png
前回の記事から流用

以上で設定はおしまいです。

ファイルツリー

今回のプロジェクトのファイルツリーを確認しておきます。
スクリーンショット 2019-12-07 17.06.06.png

処理は全て ViewController.swift に記述しますが、一部 String を拡張したのでそのコードは Extensions.swift に置きます。また、JIS0208.TXT というファイルがありますが、運転免許証に保存されている文字列のデータは JIS X 0208 でエンコードされたものです。これは Swift の String.EncodingApple Developer Documentation)には存在しないため、自力でデコードする必要があり、そのために Unicode®, Inc. が用意してくれているテキストファイルを用いてデコードします。JIS0208.TXT は私のライブラリ内の treastrain/TRETJapanNFCReader/TRETJapanNFCReader/ISO14443/DriversLicense/JIS0208.TXTUnicode.org の中 から入手できます(内容は同一)。

コード全文

いよいよコーディングです。はじめに忠告しますがとんでもないネストの深さです。エラー処理はほとんど書いていません。次のセクションでは各コードを解説しますので、読み飛ばしてもいいです。
次のセクションへジャンプ

ViewController.swift
import UIKit
import CoreNFC

class ViewController: UIViewController, NFCTagReaderSessionDelegate {

    var session: NFCTagReaderSession?
    //// 暗証番号1 **絶対に間違えないで**
    var pin1 = "1234"
    //// 暗証番号2 **絶対に間違えないで**
    var pin2 = "5678"

    override func viewDidLoad() {
        super.viewDidLoad()

        guard NFCTagReaderSession.readingAvailable else {
            print("NFC タグの読み取りに非対応のデバイス")
            return
        }

        self.session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
        self.session?.alertMessage = "運転免許証の上に iPhone の上部を載せてください"
        self.session?.begin()
    }

    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
        print("tagReaderSessionDidBecomeActive(_:)")
    }

    func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
        let readerError = error as! NFCReaderError
        print(readerError.code, readerError.localizedDescription)
    }

    func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
        print("tagReaderSession(_:didDetect:)")

        let tag = tags.first!
        session.connect(to: tag) { (error) in
            if let error = error {
                session.invalidate(errorMessage: error.localizedDescription)
                return
            }

            guard case NFCTag.iso7816(let driversLicenseCardTag) = tag else {
                session.invalidate(errorMessage: "ISO 7816 準拠ではない")
                return
            }

            session.alertMessage = "運転免許証を読み取っています…"

            /// MF を選択
            let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x00, p2Parameter: 0x00, data: Data([]), expectedResponseLength: -1)
            driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                if let error = error {
                    session.invalidate(errorMessage: error.localizedDescription)
                    return
                }

                if sw1 != 0x90 {
                    session.invalidate(errorMessage: "MF の選択でエラー: ステータス \(sw1), \(sw2)")
                    return
                }

                /// IEF01 を選択
                let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x02, p2Parameter: 0x0C, data: Data([0x00, 0x01]), expectedResponseLength: -1)
                driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                    if let error = error {
                        session.invalidate(errorMessage: error.localizedDescription)
                        return
                    }

                    if sw1 != 0x90 {
                        session.invalidate(errorMessage: "IEF01 の選択でエラー: ステータス \(sw1), \(sw2)")
                        return
                    }

                    /// 照合
                    let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0x20, p1Parameter: 0x00, p2Parameter: 0x80, data: Data(self.pin1.convertToJISX0201()), expectedResponseLength: -1)
                    driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                        if let error = error {
                            session.invalidate(errorMessage: error.localizedDescription)
                            return
                        }

                        if sw1 != 0x90 {
                            if sw1 == 0x63 {
                                if sw2 == 0x00 {
                                    session.invalidate(errorMessage: "暗証番号1の照合でエラー: 照合の不一致である")
                                } else {
                                    let remaining = sw2 - 0xC0
                                    session.invalidate(errorMessage: "暗証番号1の照合でエラー: 照合の不一致である 残り試行回数: \(remaining)")
                                }
                            } else {
                                session.invalidate(errorMessage: "暗証番号1の照合でエラー: ステータス \(sw1), \(sw2)")
                            }
                            return
                        }

                        /// IEF02 を選択
                        let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x02, p2Parameter: 0x0C, data: Data([0x00, 0x02]), expectedResponseLength: -1)
                        driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                            if let error = error {
                                session.invalidate(errorMessage: error.localizedDescription)
                                return
                            }

                            if sw1 != 0x90 {
                                session.invalidate(errorMessage: "IEF02 の選択でエラー: ステータス \(sw1), \(sw2)")
                                return
                            }

                            /// 照合
                            let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0x20, p1Parameter: 0x00, p2Parameter: 0x80, data: Data(self.pin2.convertToJISX0201()), expectedResponseLength: -1)
                            driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                                if let error = error {
                                    session.invalidate(errorMessage: error.localizedDescription)
                                    return
                                }

                                if sw1 != 0x90 {
                                    if sw1 == 0x63 {
                                        if sw2 == 0x00 {
                                            session.invalidate(errorMessage: "暗証番号2の照合でエラー: 照合の不一致である")
                                        } else {
                                            let remaining = sw2 - 0xC0
                                            session.invalidate(errorMessage: "暗証番号2の照合でエラー: 照合の不一致である 残り試行回数: \(remaining)")
                                        }
                                    } else {
                                        session.invalidate(errorMessage: "暗証番号2の照合でエラー: ステータス \(sw1), \(sw2)")
                                    }
                                    return
                                }

                                /// DF1 を選択
                                let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x04, p2Parameter: 0x0C, data: Data([0xA0, 0x00, 0x00, 0x02, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), expectedResponseLength: -1)
                                driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                                    if let error = error {
                                        session.invalidate(errorMessage: error.localizedDescription)
                                        return
                                    }

                                    if sw1 != 0x90 {
                                        session.invalidate(errorMessage: "DF1 の選択でエラー: ステータス \(sw1), \(sw2)")
                                        return
                                    }

                                    /// EF02 を選択
                                    let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x02, p2Parameter: 0x0C, data: Data([0x00, 0x02]), expectedResponseLength: -1)
                                    driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                                        if let error = error {
                                            session.invalidate(errorMessage: error.localizedDescription)
                                            return
                                        }

                                        if sw1 != 0x90 {
                                            session.invalidate(errorMessage: "DF1/EF02 の選択でエラー: ステータス \(sw1), \(sw2)")
                                            return
                                        }

                                        /// バイナリを読み取る
                                        let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xB0, p1Parameter: 0x00, p2Parameter: 0x00, data: Data([]), expectedResponseLength: 82)
                                        driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

                                            if let error = error {
                                                session.invalidate(errorMessage: error.localizedDescription)
                                                return
                                            }

                                            if sw1 != 0x90 {
                                                session.invalidate(errorMessage: "バイナリの読み取りでエラー: ステータス \(sw1), \(sw2)")
                                                return
                                            }

                                            /// TLV フィールド
                                            let tag = responseData[0]
                                            let length = Int(responseData[1])
                                            let value = responseData[2..<responseData.count].map { $0 }

                                            let registeredDomicileData = stride(from: 0, to: length, by: 2).map { (i) -> Data in
                                                var bytes = (UInt16(value[i + 1]) << 8) + UInt16(value[i])
                                                return Data(bytes: &bytes, count: MemoryLayout<UInt16>.size)
                                            }

                                            let registeredDomicile = String(jisX0208Data: registeredDomicileData)
                                            print(registeredDomicile)

                                            session.alertMessage = "完了"
                                            session.invalidate()
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
Extensions.swift
import Foundation

extension String {

    func convertToJISX0201() -> [UInt8] {
        if self.count != 4 {
            fatalError("暗証番号が4ケタではない")
        }

        let pinStringArray = Array(self)

        let pinSet = Set(pinStringArray)
        let enterableNumberSet: Set<String.Element> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*"]
        if !pinSet.isSubset(of: enterableNumberSet) {
            fatalError("暗証番号で使用できない文字が含まれている")
        }

        let pin = pinStringArray.map { (c) -> UInt8 in
            self.encodeToJISX0201(c)
        }

        return pin
    }

    func encodeToJISX0201(_ c: Character) -> UInt8 {
        switch c {
        case "0":
            return 0x30
        case "1":
            return 0x31
        case "2":
            return 0x32
        case "3":
            return 0x33
        case "4":
            return 0x34
        case "5":
            return 0x35
        case "6":
            return 0x36
        case "7":
            return 0x37
        case "8":
            return 0x38
        case "9":
            return 0x39
        case "*":
            return 0x2A
        default:
            fatalError()
        }
    }

    init(jisX0208Data: [Data]) {
        guard let path = Bundle.main.path(forResource: "JIS0208", ofType: "TXT") else {
            fatalError("JIS0208.TXT が見つかりません")
        }

        let contents = try! String(contentsOfFile: path, encoding: .utf8)
        let tableStrings = contents.components(separatedBy: .newlines)
        var tableJISX0208ToUnicode: [Data : Data] = [:]
        for row in tableStrings {
            if row.first != "#" {
                let col = row.components(separatedBy: .whitespaces)
                if col.count > 2 {
                    let col1 = col[1].hexData
                    let col2 = col[2].hexData
                    tableJISX0208ToUnicode[col1] = col2
                }
            }
        }

        var string = ""
        for data in jisX0208Data {
            if let unicodeData = tableJISX0208ToUnicode[data], let s = String(data: unicodeData, encoding: .unicode) {
                string += s
            } else {
                switch data {
                case Data([0xFF, 0xF1]):
                    string += "(外字1)"
                case Data([0xFF, 0xF2]):
                    string += "(外字2)"
                case Data([0xFF, 0xF3]):
                    string += "(外字3)"
                case Data([0xFF, 0xF4]):
                    string += "(外字4)"
                case Data([0xFF, 0xF5]):
                    string += "(外字5)"
                case Data([0xFF, 0xF6]):
                    string += "(外字6)"
                case Data([0xFF, 0xF7]):
                    string += "(外字7)"
                case Data([0xFF, 0xFA]):
                    string += "(欠字)"
                default:
                    string += "(未定義)"
                }
            }
        }

        self = string
    }

    // 参考: https://gist.github.com/eligoptimove/09ee57ac2e0c5d7889f761f40c73e9a6
    var bytes: [UInt8] {
        var i = self.startIndex
        return (0..<self.count/2).compactMap { _ in
            defer { i = self.index(i, offsetBy: 2) }
            return UInt8(self[i...index(after: i)], radix: 16)
        }
    }

    var hexData: Data {
        return Data(self.bytes)
    }
}

コード解説

それでは各コードの解説です。お手元に警察庁交通局運転免許課の運転免許証及び運転免許証作成システム等仕様書があるとよりわかりやすいかと思います。ページ番号は 仕様書バージョン番号:008 のものを言います。

対応端末かどうかを確かめる

guard NFCTagReaderSession.readingAvailable else {
    print("NFC タグの読み取りに非対応のデバイス")
    return
}

まずは Core NFC を使うときのお約束、そもそも使っている端末が読み取りに対応しているかどうかを NFCTagReaderSession.readingAvailable で確かめます。

NFCTagReaderSession をスタートさせる

self.session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
self.session?.alertMessage = "運転免許証の上に iPhone の上部を載せてください"
self.session?.begin()

対応端末であることを確認した後、NFCTagReaderSession を初期化して begin() でスタートさせます。運転免許証は ISO 14443 なので、pollingOption もそのように指定します。

NFCTagReaderSessionDelegate

func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
    print("tagReaderSessionDidBecomeActive(_:)")
}

func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
    let readerError = error as! NFCReaderError
    print(readerError.code, readerError.localizedDescription)
}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    // …
}

NFCTagReaderSessionDelegate には3つのメソッドがあり、どれも記述する必要があります。

tagReaderSessionDidBecomeActive(_:) はセッションがアクティブになったときに呼ばれます。

tagReaderSession(_:didInvalidateWithError:) はセッションがエラーで終了したときに呼ばれます。しかし、タグの読み取りが正常に終了し、session.invalidate() を呼び出すと readerSessionInvalidationErrorUserCanceled がエラーとして出力されるので、正常終了の場合、このエラーはユーザーに表示させない方法を取る必要があります。

そして、tagReaderSession(_:didDetect:) ではセッションがタグを見つけたときに呼ばれます。つまり、何かしらタグを読みたい場合はこのメソッドを起点として全ての処理を行うことになります。

タグに接続する

let tag = tags.first!
session.connect(to: tag) { (error) in
    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }

    guard case NFCTag.iso7816(let driversLicenseCardTag) = tag else {
        session.invalidate(errorMessage: "ISO 7816 準拠ではない")
        return
    }

    session.alertMessage = "運転免許証を読み取っています…"

    // …
}

まずは session.connect(to:completionHandler:) でタグに接続します。
今回は運転免許証なので、接続したタグが ISO 7816 準拠であるかを確かめ、そうでない場合は session.invalidate(errorMessage:) でセッションを終了します。

MF を選択

/// MF を選択
let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x00, p2Parameter: 0x00, data: Data([]), expectedResponseLength: -1)
driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }

    if sw1 != 0x90 {
        session.invalidate(errorMessage: "MF の選択でエラー: ステータス \(sw1), \(sw2)")
        return
    }

    // …
}

MF を選択する Application Protocol Data Unit (APDU) を作ります。
仕様書 p.2-17 によれば MF の選択は

  • instruction class (CLA): 0x00
  • instruction code (INS): 0xA4
  • P1 parameter: 0x00
  • P2 parameter: 0x00
  • データフィールド: なし
  • expected response data length (Le): -1

の APDU を sendCommand(apdu:completionHandler:) でタグに送信することで行われます。またそのレスポンスの SW1 が 0x90 以外だった場合は正常終了ではないということになるため、そこで処理を終了させます。このステータスバイト SW1 および SW2 は仕様書の p.2-18 に掲載されています。

暗証番号1を照合

/// IEF01 を選択
let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x02, p2Parameter: 0x0C, data: Data([0x00, 0x01]), expectedResponseLength: -1)
driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }

    if sw1 != 0x90 {
        session.invalidate(errorMessage: "IEF01 の選択でエラー: ステータス \(sw1), \(sw2)")
        return
    }

    /// 認証
    let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0x20, p1Parameter: 0x00, p2Parameter: 0x80, data: Data(self.pin1.convertToJISX0201()), expectedResponseLength: -1)
    driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

        if let error = error {
            session.invalidate(errorMessage: error.localizedDescription)
            return
        }

        if sw1 != 0x90 {
            if sw1 == 0x63 {
                if sw2 == 0x00 {
                    session.invalidate(errorMessage: "暗証番号1の認証でエラー: 照合の不一致である")
                } else {
                    let remaining = sw2 - 0xC0
                    session.invalidate(errorMessage: "暗証番号1の認証でエラー: 照合の不一致である 残り試行回数: \(remaining)")
                }
            } else {
                session.invalidate(errorMessage: "暗証番号1の認証でエラー: ステータス \(sw1), \(sw2)")
            }
            return
        }

        // …
    }
}

IEF01 を選択する APDU を作ります。

  • instruction class (CLA): 0x00
  • instruction code (INS): 0xA4
  • P1 parameter: 0x02
  • P2 parameter: 0x0C
  • データフィールド: 0x00, 0x01
  • expected response data length (Le): -1

P1 パラメータを 0x02、P2 パラメータを 0x0C とすることで EF-ID による直接選択を行うことにし、データフィールドで IEF01 を指定しています。

その後、暗証番号1との照合を行います。照合を行う APDU は

  • instruction class (CLA): 0x00
  • instruction code (INS): 0x20
  • P1 parameter: 0x00
  • P2 parameter: 0x80
  • データフィールド: 暗証番号1を JIS X 0201 でエンコードしたもの
  • expected response data length (Le): -1

となります。データフィールドに入れる暗証番号1は JIS X 0201 でエンコードしたデータ となります。ここで使用した convertToJISX0201()Extensions.swift に書いたものです。
照合のコマンドで返されるステータスバイト SW1 および SW2 は仕様書の p.2-20 に掲載されています。もし暗証番号1が間違っている場合は SW2 に残り試行回数の情報が返されます。

暗証番号2を照合

/// IEF02 を選択
let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x02, p2Parameter: 0x0C, data: Data([0x00, 0x02]), expectedResponseLength: -1)
driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }

    if sw1 != 0x90 {
        session.invalidate(errorMessage: "IEF02 の選択でエラー: ステータス \(sw1), \(sw2)")
        return
    }

    /// 認証
    let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0x20, p1Parameter: 0x00, p2Parameter: 0x80, data: Data(self.pin2.convertToJISX0201()), expectedResponseLength: -1)
    driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

        if let error = error {
            session.invalidate(errorMessage: error.localizedDescription)
            return
        }

        if sw1 != 0x90 {
            if sw1 == 0x63 {
                if sw2 == 0x00 {
                    session.invalidate(errorMessage: "暗証番号2の認証でエラー: 照合の不一致である")
                } else {
                    let remaining = sw2 - 0xC0
                    session.invalidate(errorMessage: "暗証番号2の認証でエラー: 照合の不一致である 残り試行回数: \(remaining)")
                }
            } else {
                session.invalidate(errorMessage: "暗証番号2の認証でエラー: ステータス \(sw1), \(sw2)")
            }
            return
        }

        // …
    }
}

行うことは暗証番号1の照合と同じなので、説明は省略します。

  • IEF02 を選択する APDU
    • instruction class (CLA): 0x00
    • instruction code (INS): 0xA4
    • P1 parameter: 0x02
    • P2 parameter: 0x0C
    • データフィールド: 0x00, 0x02
    • expected response data length (Le): -1
  • 暗証番号2を照合する APDU
    • instruction class (CLA): 0x00
    • instruction code (INS): 0x20
    • P1 parameter: 0x00
    • P2 parameter: 0x80
    • データフィールド: 暗証番号2を JIS X 0201 でエンコードしたもの
    • expected response data length (Le): -1

DF1/EF02 を選択

/// DF1 を選択
let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x04, p2Parameter: 0x0C, data: Data([0xA0, 0x00, 0x00, 0x02, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), expectedResponseLength: -1)
driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }

    if sw1 != 0x90 {
        session.invalidate(errorMessage: "DF1 の選択でエラー: ステータス \(sw1), \(sw2)")
        return
    }

    /// EF02 を選択
    let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xA4, p1Parameter: 0x02, p2Parameter: 0x0C, data: Data([0x00, 0x02]), expectedResponseLength: -1)
    driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

        if let error = error {
            session.invalidate(errorMessage: error.localizedDescription)
            return
        }

        if sw1 != 0x90 {
            session.invalidate(errorMessage: "DF1/EF02 の選択でエラー: ステータス \(sw1), \(sw2)")
            return
        }

        // …
    }
}
  • DF1 を選択する APDU
    • instruction class (CLA): 0x00
    • instruction code (INS): 0xA4
    • P1 parameter: 0x04
    • P2 parameter: 0x0C
    • データフィールド: 0xA0, 0x00, 0x00, 0x02, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    • expected response data length (Le): -1
  • EF02 を選択する APDU
    • instruction class (CLA): 0x00
    • instruction code (INS): 0xA4
    • P1 parameter: 0x02
    • P2 parameter: 0x0C
    • データフィールド: 0x00, 0x02
    • expected response data length (Le): -1

DF1 を選択する APDU は、P1 パラメータを 0x04、P2 パラメータを 0x0C とすることで DF 名による直接選択を行うことにし、データフィールドで DF2 の AID を指定しています。
EF02 を選択する APDU は、P1 パラメータを 0x02、P2 パラメータを 0x0C とすることで EF-ID による直接選択を行うことにし、データフィールドで EF02 を指定しています。

バイナリを読み取る

/// バイナリを読み取る
let adpu = NFCISO7816APDU(instructionClass: 0x00, instructionCode: 0xB0, p1Parameter: 0x00, p2Parameter: 0x00, data: Data([]), expectedResponseLength: 82)
driversLicenseCardTag.sendCommand(apdu: adpu) { (responseData, sw1, sw2, error) in

    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }

    if sw1 != 0x90 {
        session.invalidate(errorMessage: "バイナリの読み取りでエラー: ステータス \(sw1), \(sw2)")
        return
    }

    // …
}

バイナリを読み取る APDU は仕様書 p.2-21

  • instruction class (CLA): 0x00
  • instruction code (INS): 0xB0
  • P1 parameter: 0x00
  • P2 parameter: 0x00
  • データフィールド: なし
  • expected response data length (Le): 82

となります。

TLV レコードを作成

/// TLV フィールド
let tag = responseData[0]
let length = Int(responseData[1])
let value = responseData[2..<responseData.count].map { $0 }

仕様書 p.2-3 に従って TLV レコードを作ります。と、言っても今回は本籍しか読み取らないので、タグ部分は使いません。

JIS X 0208 でデコードして終了

let registeredDomicileData = stride(from: 0, to: length, by: 2).map { (i) -> Data in
    var bytes = (UInt16(value[i + 1]) << 8) + UInt16(value[i])
    return Data(bytes: &bytes, count: MemoryLayout<UInt16>.size)
}

let registeredDomicile = String(jisX0208Data: registeredDomicileData)
print(registeredDomicile)

session.alertMessage = "完了"
session.invalidate()

取得できるデータは JIS X 0208 でエンコードされているものなので、これを String に変換します。ここでの init(jisX0208Data: [Data])Extensions.swift に書いたものです。

スクリーンショット 2019-12-07 16.57.07.png

後記

大変お疲れさまでした。もう自分で実装するのはやめてライブラリ使いませんか…?

あと、暗証番号は絶対に間違えないでください。前回の記事でも記述しましたが、暗証番号は3回間違えると免許証がロックされ、警察署等でロック解除をしてもらわなければ運転免許証を読み取ることができなくなってしまいます。試す際は自己責任でお願いします。

ここでのコードは Gist にも載せておきました。参考にしてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away