Last active
December 7, 2019 11:21
-
-
Save treastrain/5a6196915374f430951a4a768a1b1402 to your computer and use it in GitHub Desktop.
Core NFC (iOS 13.0 以降) で運転免許証の本籍を読み取る
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Extensions.swift | |
// driversLicenceReader | |
// | |
// Created by treastrain on 2019/12/06. | |
// Copyright © 2019 treastrain / Tanaka Ryoga. All rights reserved. | |
// | |
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ViewController.swift | |
// driversLicenceReader | |
// | |
// Created by treastrain on 2019/12/06. | |
// Copyright © 2019 treastrain / Tanaka Ryoga. All rights reserved. | |
// | |
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() | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment