Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 101 additions & 29 deletions Examples/Example_SPM/NFCPassportReaderApp/Views/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ struct MainView : View {
@State private var showScanMRZ : Bool = false
@State private var showSavedPassports : Bool = false
@State private var gettingLogs : Bool = false
@State private var selectedPasswordType: PACEPasswordType = .mrz
@State private var canNumber = ""

@State var page = 0

Expand All @@ -53,13 +55,49 @@ struct MainView : View {
}).navigationTitle("Scan MRZ"), isActive: $showScanMRZ){ Text("") }

VStack {
HStack {
Spacer()
Button(action: {self.showScanMRZ.toggle()}) {
Label("Scan MRZ", systemImage:"camera")
}.padding([.top, .trailing])
// Authentication Method Picker
Picker("Authentication Method", selection: $selectedPasswordType) {
Text("MRZ").tag(PACEPasswordType.mrz)
Text("CAN").tag(PACEPasswordType.can)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
.padding(.top)

// Show appropriate UI based on selection
if selectedPasswordType == .mrz {
HStack {
Spacer()
Button(action: {self.showScanMRZ.toggle()}) {
Label("Scan MRZ", systemImage:"camera")
}.padding([.top, .trailing])
}
MRZEntryView()
} else {
VStack(alignment: .leading) {
Text("Card Access Number (CAN)")
.font(.headline)
.padding(.horizontal)
.padding(.top)

TextField("Enter 6-digit CAN", text: $canNumber)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
.onChange(of: canNumber) { newValue in
// Limit to 6 digits and ensure only digits
if newValue.count > 6 {
canNumber = String(newValue.prefix(6))
}
canNumber = newValue.filter { $0.isNumber }
}

Text("The CAN is a 6-digit number printed on your document")
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal)
}
}
MRZEntryView()

Button(action: {
self.scanPassport()
Expand Down Expand Up @@ -134,7 +172,12 @@ struct MainView : View {
extension MainView {

var isValid : Bool {
return settings.passportNumber.count >= 8
// Updated to handle both MRZ and CAN validation
if selectedPasswordType == .mrz {
return settings.passportNumber.count >= 8
} else {
return canNumber.count == 6 && canNumber.allSatisfy { $0.isNumber }
}
}

func parse( mrz:String ) -> (String, Date, Date)? {
Expand Down Expand Up @@ -170,17 +213,26 @@ extension MainView {
hideKeyboard()
self.showDetails = false

let df = DateFormatter()
df.timeZone = TimeZone(secondsFromGMT: 0)
df.dateFormat = "YYMMdd"
// Key difference: Prepare parameters based on selected authentication type
let mrzKeyParam: String?
let canParam: String?

let pptNr = settings.passportNumber
let dob = df.string(from:settings.dateOfBirth)
let doe = df.string(from:settings.dateOfExpiry)
let useExtendedMode = settings.useExtendedMode

let passportUtils = PassportUtils()
let mrzKey = passportUtils.getMRZKey( passportNumber: pptNr, dateOfBirth: dob, dateOfExpiry: doe)
if selectedPasswordType == .mrz {
let df = DateFormatter()
df.timeZone = TimeZone(secondsFromGMT: 0)
df.dateFormat = "YYMMdd"

let pptNr = settings.passportNumber
let dob = df.string(from:settings.dateOfBirth)
let doe = df.string(from:settings.dateOfExpiry)

let passportUtils = PassportUtils()
mrzKeyParam = passportUtils.getMRZKey(passportNumber: pptNr, dateOfBirth: dob, dateOfExpiry: doe)
canParam = nil
} else {
mrzKeyParam = nil
canParam = canNumber
}

// Set the masterListURL on the Passport Reader to allow auto passport verification
let masterListURL = Bundle.main.url(forResource: "masterList", withExtension: ".pem")!
Expand All @@ -196,18 +248,38 @@ extension MainView {
appLogging.error( "Using version \(UIApplication.version)" )

Task {
let customMessageHandler : (NFCViewDisplayMessage)->String? = { (displayMessage) in
switch displayMessage {
case .requestPresentPassport:
return "Hold your iPhone near an NFC enabled passport."
default:
// Return nil for all other messages so we use the provided default
return nil
let customMessageHandler: (NFCViewDisplayMessage)->String? = { (displayMessage) in
switch displayMessage {
case .requestPresentPassport:
return selectedPasswordType == .mrz ?
"Hold your iPhone near an NFC enabled passport." :
"Hold your iPhone near the document and enter the CAN."
default:
// Return nil for all other messages so we use the provided default
return nil
}
}
}

do {
let passport = try await passportReader.readPassport( mrzKey: mrzKey, useExtendedMode: useExtendedMode, customDisplayMessage:customMessageHandler)

do {
let passport: NFCPassportModel

if selectedPasswordType == .mrz {
// Use the original API for MRZ to demonstrate backward compatibility
passport = try await passportReader.readPassport(
mrzKey: mrzKeyParam,
useExtendedMode: settings.useExtendedMode,
customDisplayMessage: customMessageHandler
)
} else {
// Use the new API for CAN
passport = try await passportReader.readPassport(
mrzKey: nil,
can: canParam,
useExtendedMode: settings.useExtendedMode,
customDisplayMessage: customMessageHandler
)
}


if let _ = passport.faceImageInfo {
print( "Got face Image details")
Expand All @@ -230,7 +302,7 @@ extension MainView {
}
} catch {
self.alertTitle = "Oops"
self.alertTitle = error.localizedDescription
self.alertMessage = error.localizedDescription
self.showingAlert = true

}
Expand Down
2 changes: 2 additions & 0 deletions Sources/NFCPassportReader/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum NFCPassportReaderError: Error {
case InvalidDataPassed(String)
case NotYetSupported(String)
case Unknown(Error)
case InvalidCAN

var value: String {
switch self {
Expand Down Expand Up @@ -77,6 +78,7 @@ public enum NFCPassportReaderError: Error {
case .InvalidDataPassed(let reason) : return "Invalid data passed - \(reason)"
case .NotYetSupported(let reason) : return "Not yet supported - \(reason)"
case .Unknown(let error): return "Unknown error: \(error.localizedDescription)"
case .InvalidCAN: return "InvalidCAN"
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/NFCPassportReader/NFCViewDisplayMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum NFCViewDisplayMessage {
case error(NFCPassportReaderError)
case activeAuthentication
case successfulRead
case requestPresentPassportForCAN
}

@available(iOS 13, macOS 10.15, *)
Expand All @@ -39,6 +40,13 @@ extension NFCViewDisplayMessage {
return "Connection error. Please try again."
case NFCPassportReaderError.InvalidMRZKey:
return "MRZ Key not valid for this document."
case NFCPassportReaderError.InvalidCAN:
return "CAN is not valid for this document."
case NFCPassportReaderError.InvalidDataPassed(let reason):
if reason.contains("CAN") {
return "Invalid CAN: \(reason)"
}
return "Invalid data: \(reason)"
case NFCPassportReaderError.ResponseError(let description, let sw1, let sw2):
return "Sorry, there was a problem reading the passport. \(description) - (0x\(sw1), 0x\(sw2)"
default:
Expand All @@ -48,6 +56,8 @@ extension NFCViewDisplayMessage {
return "Authenticating....."
case .successfulRead:
return "Passport read successfully"
case .requestPresentPassportForCAN:
return "Hold your iPhone near an NFC enabled passport and enter your CAN."
}
}

Expand Down
50 changes: 38 additions & 12 deletions Sources/NFCPassportReader/PACEHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ extension PACEHandlerError: LocalizedError {
}
}

public enum PACEPasswordType {
case mrz
case can
// Other types could be added: PIN, PUK, etc.
}

@available(iOS 15, *)
public class PACEHandler {

Expand Down Expand Up @@ -72,7 +78,7 @@ public class PACEHandler {
isPACESupported = true
}

public func doPACE( mrzKey : String ) async throws {
public func doPACE(password: String, passwordType: PACEPasswordType = .mrz) async throws {
guard isPACESupported else {
throw NFCPassportReaderError.NotYetSupported( "PACE not supported" )
}
Expand All @@ -88,8 +94,16 @@ public class PACEHandler {
digestAlg = try paceInfo.getDigestAlgorithm() // Either SHA-1 or SHA-256.
keyLength = try paceInfo.getKeyLength() // Get key length the enc cipher. Either 128, 192, or 256.

paceKeyType = PACEHandler.MRZ_PACE_KEY_REFERENCE
paceKey = try createPaceKey( from: mrzKey )
// Set key reference based on password type
switch passwordType {
case .mrz:
paceKeyType = PACEHandler.MRZ_PACE_KEY_REFERENCE
case .can:
paceKeyType = PACEHandler.CAN_PACE_KEY_REFERENCE
}

// Create PACE key with appropriate method based on type
paceKey = try createPaceKey(from: password, type: passwordType)

// Temporary logging
Logger.pace.debug("doPace - inpit parameters" )
Expand All @@ -100,7 +114,7 @@ public class PACEHandler {
Logger.pace.debug("cipherAlg - \(self.cipherAlg)" )
Logger.pace.debug("digestAlg - \(self.digestAlg)" )
Logger.pace.debug("keyLength - \(self.keyLength)" )
Logger.pace.debug("keyLength - \(mrzKey)" )
Logger.pace.debug("keyLength - \(password)" )
Logger.pace.debug("paceKey - \(binToHexRep(self.paceKey, asArray:true))" )

// First start the initial auth call
Expand Down Expand Up @@ -580,15 +594,27 @@ extension PACEHandler {
}

/// Computes a key seed based on an MRZ key
/// - Parameter the mrz key
/// - Parameters:
/// - password: The password to be used for PACE
/// - PACEPasswordType: The type of the password for example MRZ, CAN, etc
/// - Returns a encoded key based on the mrz key that can be used for PACE
func createPaceKey( from mrzKey: String ) throws -> [UInt8] {
let buf: [UInt8] = Array(mrzKey.utf8)
let hash = calcSHA1Hash(buf)

let smskg = SecureMessagingSessionKeyGenerator()
let key = try smskg.deriveKey(keySeed: hash, cipherAlgName: cipherAlg, keyLength: keyLength, nonce: nil, mode: .PACE_MODE, paceKeyReference: paceKeyType)
return key
func createPaceKey(from password: String, type: PACEPasswordType) throws -> [UInt8] {
switch type {
case .mrz:
let buf: [UInt8] = Array(password.utf8)
let hash = calcSHA1Hash(buf)

let smskg = SecureMessagingSessionKeyGenerator()
let key = try smskg.deriveKey(keySeed: hash, cipherAlgName: cipherAlg, keyLength: keyLength, nonce: nil, mode: .PACE_MODE, paceKeyReference: paceKeyType)
return key

case .can:
let canBytes: [UInt8] = Array(password.utf8)

let smskg = SecureMessagingSessionKeyGenerator()
let key = try smskg.deriveKey(keySeed: canBytes, cipherAlgName: cipherAlg, keyLength: keyLength, nonce: nil, mode: .PACE_MODE, paceKeyReference: paceKeyType)
return key
}
}

/// Performs the ECDH PACE GM key agreement protocol by multiplying a private key with a public key
Expand Down
21 changes: 21 additions & 0 deletions Sources/NFCPassportReader/PACEPasswordValidator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// PACEPasswordValidator.swift
// NFCPassportReader
//
// Created by Manwel Bugeja Personal on 24/04/2025.
//


class PACEPasswordValidator {
static func validate(password: String, type: PACEPasswordType) throws {
switch type {
case .mrz:
// MRZ validation logic if needed
break
case .can:
guard password.count == 6 && password.allSatisfy({ $0.isNumber }) else {
throw NFCPassportReaderError.InvalidDataPassed("CAN must be 6 digits")
}
}
}
}
Loading