Files
self/app/ios/AwesomeProject/PassportReader.swift
Rémi Colin afc235a592 check
2024-01-10 10:52:10 +01:00

406 lines
18 KiB
Swift

import Foundation
#if !os(macOS)
import UIKit
import CoreNFC
@available(iOS 15, *)
public class PassportReader : NSObject {
private typealias NFCCheckedContinuation = CheckedContinuation<NFCPassportModel, Error>
private var nfcContinuation: NFCCheckedContinuation?
private var passport : NFCPassportModel = NFCPassportModel()
private var readerSession: NFCTagReaderSession?
private var currentlyReadingDataGroup : DataGroupId?
private var dataGroupsToRead : [DataGroupId] = []
private var readAllDatagroups = false
private var skipSecureElements = true
private var skipCA = false
private var skipPACE = false
private var bacHandler : BACHandler?
private var caHandler : ChipAuthenticationHandler?
private var paceHandler : PACEHandler?
private var mrzKey : String = ""
private var dataAmountToReadOverride : Int? = nil
private var scanCompletedHandler: ((NFCPassportModel?, NFCPassportReaderError?)->())!
private var nfcViewDisplayMessageHandler: ((NFCViewDisplayMessage) -> String?)?
private var masterListURL : URL?
private var shouldNotReportNextReaderSessionInvalidationErrorUserCanceled : Bool = false
// By default, Passive Authentication uses the new RFS5652 method to verify the SOD, but can be switched to use
// the previous OpenSSL CMS verification if necessary
public var passiveAuthenticationUsesOpenSSL : Bool = false
public init( logLevel: LogLevel = .info, masterListURL: URL? = nil ) {
super.init()
Log.logLevel = logLevel
self.masterListURL = masterListURL
}
public func setMasterListURL( _ masterListURL : URL ) {
self.masterListURL = masterListURL
}
// This function allows you to override the amount of data the TagReader tries to read from the NFC
// chip. NOTE - this really shouldn't be used for production but is useful for testing as different
// passports support different data amounts.
// It appears that the most reliable is 0xA0 (160 chars) but some will support arbitary reads (0xFF or 256)
public func overrideNFCDataAmountToRead( amount: Int ) {
dataAmountToReadOverride = amount
}
public func readPassport( mrzKey : String, tags : [DataGroupId] = [], skipSecureElements : Bool = true, skipCA : Bool = false, skipPACE : Bool = false, customDisplayMessage : ((NFCViewDisplayMessage) -> String?)? = nil) async throws -> NFCPassportModel {
self.passport = NFCPassportModel()
self.mrzKey = mrzKey
self.skipCA = skipCA
self.skipPACE = skipPACE
self.dataGroupsToRead.removeAll()
self.dataGroupsToRead.append( contentsOf:tags)
self.nfcViewDisplayMessageHandler = customDisplayMessage
self.skipSecureElements = skipSecureElements
self.currentlyReadingDataGroup = nil
self.bacHandler = nil
self.caHandler = nil
self.paceHandler = nil
// If no tags specified, read all
if self.dataGroupsToRead.count == 0 {
// Start off with .COM, will always read (and .SOD but we'll add that after), and then add the others from the COM
self.dataGroupsToRead.append(contentsOf:[.COM, .SOD] )
self.readAllDatagroups = true
} else {
// We are reading specific datagroups
self.readAllDatagroups = false
}
guard NFCNDEFReaderSession.readingAvailable else {
throw NFCPassportReaderError.NFCNotSupported
}
if NFCTagReaderSession.readingAvailable {
readerSession = NFCTagReaderSession(pollingOption: [.iso14443], delegate: self, queue: nil)
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.requestPresentPassport )
readerSession?.begin()
}
return try await withCheckedThrowingContinuation({ (continuation: NFCCheckedContinuation) in
self.nfcContinuation = continuation
})
}
}
@available(iOS 15, *)
extension PassportReader : NFCTagReaderSessionDelegate {
// MARK: - NFCTagReaderSessionDelegate
public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
// If necessary, you may perform additional operations on session start.
// At this point RF polling is enabled.
Log.debug( "tagReaderSessionDidBecomeActive" )
}
public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
// If necessary, you may handle the error. Note session is no longer valid.
// You must create a new session to restart RF polling.
Log.debug( "tagReaderSession:didInvalidateWithError - \(error.localizedDescription)" )
self.readerSession?.invalidate()
self.readerSession = nil
if let readerError = error as? NFCReaderError, readerError.code == NFCReaderError.readerSessionInvalidationErrorUserCanceled
&& self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled {
self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled = false
} else {
var userError = NFCPassportReaderError.UnexpectedError
if let readerError = error as? NFCReaderError {
Log.error( "tagReaderSession:didInvalidateWithError - Got NFCReaderError - \(readerError.localizedDescription)" )
switch (readerError.code) {
case NFCReaderError.readerSessionInvalidationErrorUserCanceled:
Log.error( " - User cancelled session" )
userError = NFCPassportReaderError.UserCanceled
default:
Log.error( " - some other error - \(readerError.localizedDescription)" )
userError = NFCPassportReaderError.UnexpectedError
}
} else {
Log.error( "tagReaderSession:didInvalidateWithError - Received error - \(error.localizedDescription)" )
}
nfcContinuation?.resume(throwing: userError)
nfcContinuation = nil
}
}
public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
Log.debug( "tagReaderSession:didDetect - \(tags[0])" )
if tags.count > 1 {
Log.debug( "tagReaderSession:more than 1 tag detected! - \(tags)" )
let errorMessage = NFCViewDisplayMessage.error(.MoreThanOneTagFound)
self.invalidateSession(errorMessage: errorMessage, error: NFCPassportReaderError.MoreThanOneTagFound)
return
}
let tag = tags.first!
var passportTag: NFCISO7816Tag
switch tags.first! {
case let .iso7816(tag):
passportTag = tag
default:
Log.debug( "tagReaderSession:invalid tag detected!!!" )
let errorMessage = NFCViewDisplayMessage.error(NFCPassportReaderError.TagNotValid)
self.invalidateSession(errorMessage:errorMessage, error: NFCPassportReaderError.TagNotValid)
return
}
Task { [passportTag] in
do {
try await session.connect(to: tag)
Log.debug( "tagReaderSession:connected to tag - starting authentication" )
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.authenticatingWithPassport(0) )
let tagReader = TagReader(tag:passportTag)
if let newAmount = self.dataAmountToReadOverride {
tagReader.overrideDataAmountToRead(newAmount: newAmount)
}
tagReader.progress = { [unowned self] (progress) in
if let dgId = self.currentlyReadingDataGroup {
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(dgId, progress) )
} else {
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.authenticatingWithPassport(progress) )
}
}
let passportModel = try await self.startReading( tagReader : tagReader)
nfcContinuation?.resume(returning: passportModel)
nfcContinuation = nil
} catch let error as NFCPassportReaderError {
let errorMessage = NFCViewDisplayMessage.error(error)
self.invalidateSession(errorMessage: errorMessage, error: error)
} catch let error {
nfcContinuation?.resume(throwing: error)
nfcContinuation = nil
Log.debug( "tagReaderSession:failed to connect to tag - \(error.localizedDescription)" )
let errorMessage = NFCViewDisplayMessage.error(NFCPassportReaderError.ConnectionError)
self.invalidateSession(errorMessage: errorMessage, error: NFCPassportReaderError.ConnectionError)
}
}
}
func updateReaderSessionMessage(alertMessage: NFCViewDisplayMessage ) {
self.readerSession?.alertMessage = self.nfcViewDisplayMessageHandler?(alertMessage) ?? alertMessage.description
}
}
@available(iOS 15, *)
extension PassportReader {
func startReading(tagReader : TagReader) async throws -> NFCPassportModel {
if !skipPACE {
do {
let data = try await tagReader.readCardAccess()
Log.verbose( "Read CardAccess - data \(binToHexRep(data))" )
let cardAccess = try CardAccess(data)
passport.cardAccess = cardAccess
Log.info( "Starting Password Authenticated Connection Establishment (PACE)" )
let paceHandler = try PACEHandler( cardAccess: cardAccess, tagReader: tagReader )
try await paceHandler.doPACE(mrzKey: mrzKey )
passport.PACEStatus = .success
Log.debug( "PACE Succeeded" )
} catch {
passport.PACEStatus = .failed
Log.error( "PACE Failed - falling back to BAC" )
}
_ = try await tagReader.selectPassportApplication()
}
// If either PACE isn't supported, we failed whilst doing PACE or we didn't even attempt it, then fall back to BAC
if passport.PACEStatus != .success {
try await doBACAuthentication(tagReader : tagReader)
}
// Now to read the datagroups
try await readDataGroups(tagReader: tagReader)
self.updateReaderSessionMessage(alertMessage: NFCViewDisplayMessage.successfulRead)
try await doActiveAuthenticationIfNeccessary(tagReader : tagReader)
self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled = true
self.readerSession?.invalidate()
// If we have a masterlist url set then use that and verify the passport now
self.passport.verifyPassport(masterListURL: self.masterListURL, useCMSVerification: self.passiveAuthenticationUsesOpenSSL)
return self.passport
}
func doActiveAuthenticationIfNeccessary( tagReader : TagReader) async throws {
guard self.passport.activeAuthenticationSupported else {
return
}
Log.info( "Performing Active Authentication" )
let challenge = generateRandomUInt8Array(8)
Log.verbose( "Generated Active Authentication challange - \(binToHexRep(challenge))")
let response = try await tagReader.doInternalAuthentication(challenge: challenge)
self.passport.verifyActiveAuthentication( challenge:challenge, signature:response.data )
}
func doBACAuthentication(tagReader : TagReader) async throws {
self.currentlyReadingDataGroup = nil
Log.info( "Starting Basic Access Control (BAC)" )
self.passport.BACStatus = .failed
self.bacHandler = BACHandler( tagReader: tagReader )
try await bacHandler?.performBACAndGetSessionKeys( mrzKey: mrzKey )
Log.info( "Basic Access Control (BAC) - SUCCESS!" )
self.passport.BACStatus = .success
}
func readDataGroups( tagReader: TagReader ) async throws {
// Read COM
var DGsToRead = [DataGroupId]()
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(.COM, 0) )
if let com = try await readDataGroup(tagReader:tagReader, dgId:.COM) as? COM {
self.passport.addDataGroup( .COM, dataGroup:com )
// SOD and COM shouldn't be present in the DG list but just in case (worst case here we read the sod twice)
DGsToRead = [.SOD] + com.dataGroupsPresent.map { DataGroupId.getIDFromName(name:$0) }
DGsToRead.removeAll { $0 == .COM }
}
if DGsToRead.contains( .DG14 ) {
DGsToRead.removeAll { $0 == .DG14 }
if !skipCA {
// Do Chip Authentication
if let dg14 = try await readDataGroup(tagReader:tagReader, dgId:.DG14) as? DataGroup14 {
self.passport.addDataGroup( .DG14, dataGroup:dg14 )
let caHandler = ChipAuthenticationHandler(dg14: dg14, tagReader: tagReader)
if caHandler.isChipAuthenticationSupported {
do {
// Do Chip authentication and then continue reading datagroups
try await caHandler.doChipAuthentication()
self.passport.chipAuthenticationStatus = .success
} catch {
Log.info( "Chip Authentication failed - re-establishing BAC")
self.passport.chipAuthenticationStatus = .failed
// Failed Chip Auth, need to re-establish BAC
try await doBACAuthentication(tagReader: tagReader)
}
}
}
}
}
// If we are skipping secure elements then remove .DG3 and .DG4
if self.skipSecureElements {
DGsToRead = DGsToRead.filter { $0 != .DG3 && $0 != .DG4 }
}
if self.readAllDatagroups != true {
DGsToRead = DGsToRead.filter { dataGroupsToRead.contains($0) }
}
for dgId in DGsToRead {
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(dgId, 0) )
if let dg = try await readDataGroup(tagReader:tagReader, dgId:dgId) {
self.passport.addDataGroup( dgId, dataGroup:dg )
}
}
}
func readDataGroup( tagReader : TagReader, dgId : DataGroupId ) async throws -> DataGroup? {
self.currentlyReadingDataGroup = dgId
Log.info( "Reading tag - \(dgId)" )
var readAttempts = 0
self.updateReaderSessionMessage( alertMessage: NFCViewDisplayMessage.readingDataGroupProgress(dgId, 0) )
repeat {
do {
let response = try await tagReader.readDataGroup(dataGroup:dgId)
let dg = try DataGroupParser().parseDG(data: response)
return dg
} catch let error as NFCPassportReaderError {
Log.error( "TagError reading tag - \(error)" )
// OK we had an error - depending on what happened, we may want to try to re-read this
// E.g. we failed to read the last Datagroup because its protected and we can't
let errMsg = error.value
Log.error( "ERROR - \(errMsg)" )
var redoBAC = false
if errMsg == "Session invalidated" || errMsg == "Class not supported" || errMsg == "Tag connection lost" {
// Check if we have done Chip Authentication, if so, set it to nil and try to redo BAC
if self.caHandler != nil {
self.caHandler = nil
redoBAC = true
} else {
// Can't go any more!
throw error
}
} else if errMsg == "Security status not satisfied" || errMsg == "File not found" {
// Can't read this element as we aren't allowed - remove it and return out so we re-do BAC
self.dataGroupsToRead.removeFirst()
redoBAC = true
} else if errMsg == "SM data objects incorrect" || errMsg == "Class not supported" {
// Can't read this element security objects now invalid - and return out so we re-do BAC
redoBAC = true
} else if errMsg.hasPrefix( "Wrong length" ) || errMsg.hasPrefix( "End of file" ) { // Should now handle errors 0x6C xx, and 0x67 0x00
// OK passport can't handle max length so drop it down
tagReader.reduceDataReadingAmount()
redoBAC = true
}
if redoBAC {
// Redo BAC and try again
try await doBACAuthentication(tagReader : tagReader)
} else {
// Some other error lets have another try
}
}
readAttempts += 1
} while ( readAttempts < 2 )
return nil
}
func invalidateSession(errorMessage: NFCViewDisplayMessage, error: NFCPassportReaderError) {
// Mark the next 'invalid session' error as not reportable (we're about to cause it by invalidating the
// session). The real error is reported back with the call to the completed handler
self.shouldNotReportNextReaderSessionInvalidationErrorUserCanceled = true
self.readerSession?.invalidate(errorMessage: self.nfcViewDisplayMessageHandler?(errorMessage) ?? errorMessage.description)
nfcContinuation?.resume(throwing: error)
nfcContinuation = nil
}
}
#endif