diff --git a/README.md b/README.md index 4d635dc0a..339af0b02 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Users can also selectively disclose personal info like their nationality or thei - ✅ Basic passport verifier circuit - 🚧 Optimization -- 🚧 Selective disclosure +- ✅ Selective disclosure - ✅ Basic react native frontend -- 🚧 Passport verification pipeline, android -- 🚧 Passport verification pipeline, iOS +- ✅ Passport verification pipeline, android +- ✅ Passport verification pipeline, iOS - 🚧 Contracts - 🚧 On-chain registry of CSCA pubkeys based on the official ICAO masterlist \ No newline at end of file diff --git a/app/ios/PassportReader.swift b/app/ios/PassportReader.swift index 2b2b33fa7..8db10d953 100644 --- a/app/ios/PassportReader.swift +++ b/app/ios/PassportReader.swift @@ -1,9 +1,3 @@ -// -// PassportReader.swift -// AwesomeProject -// -// Created by Y E on 27/07/2023. -// import Foundation import React @@ -12,6 +6,55 @@ import NFCPassportReader @available(iOS 15, *) @objc(PassportReader) class PassportReader: NSObject{ + + private typealias NFCCheckedContinuation = CheckedContinuation + 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 + } + private let passportReader = NFCPassportReader.PassportReader() @@ -87,15 +130,204 @@ class PassportReader: NSObject{ } } -// @objc(scanPassport:dateOfBirth:dateOfExpiry:resolve:reject:) -// func scanPassport(_ passportNumber: String, dateOfBirth: String, dateOfExpiry: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { -// let concatenatedString = "\(passportNumber), \(dateOfBirth), \(dateOfExpiry)" -// resolve(concatenatedString) -// } + @objc(scanPassport:dateOfBirth:dateOfExpiry:resolve:reject:) + func scanPassport(_ passportNumber: String, dateOfBirth: String, dateOfExpiry: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + let concatenatedString = "\(passportNumber), \(dateOfBirth), \(dateOfExpiry)" + resolve(concatenatedString) + } @objc static func requiresMainQueueSetup() -> Bool { return true } + + + + 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 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 ) + } + } + } + }