Complete guide for NFC passport reading functionality in the OkID iOS SDK.
The NFC module enables reading biometric data from ePassports (MRTD - Machine Readable Travel Documents) using:
- MRZ OCR: Automatic extraction of document number, DOB, and expiry dates from passport's Machine Readable Zone
- NFC Reading: Secure reading of passport chip data using PACE (Password Authenticated Connection Establishment) or BAC (Basic Access Control)
- DG2 Parsing: Extraction of facial biometric data (photo) from Data Group 2
###NFC Architecture
┌─────────────────────────────────────────────────────────────┐
│ NFC Module │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MRZ Scanner │────▶│ NFC Input │────▶│ NFC Reading │ │
│ │ (OCR) │ │ (Manual) │ │ (CoreNFC) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────┬────┴─────────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ NFC Result │ │
│ │ (Display) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Extracts and validates passport data from MRZ (Machine Readable Zone).
Features:
- TD3 format parsing (2-line passport MRZ)
- Check digit validation (7-3-1 weighted algorithm)
- OCR error correction (common substitutions: O↔0, I↔1, etc.)
- Field extraction: document number, DOB, expiry date
- Cross-frame confidence tracking
Key Methods:
// Parse MRZ line 2 (contains data)
let parsed = MRZParser.parseLine2(mrzText)
let docNumber = parsed["documentNumber"]
let dob = parsed["dateOfBirth"] // YYMMDD format
let expiry = parsed["dateOfExpiry"] // YYMMDD format
// Validate MRZ line 1 (passport format check)
if MRZParser.isPassportMRZLine1(line1) {
// Valid passport MRZ detected
}
// Validate MRZ line 2 pattern
if MRZParser.looksLikeMRZLine2(line2) {
// Potential data line detected
}
// OCR error correction
let corrected = MRZParser.correctOCRErrors(rawOCRText)MRZ Format (TD3):
Line 1: P<NLDTEST<<JOHN<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
│││ │
││└───┴── Names (separated by <<)
│└────── Country code (3 letters)
└─────── Document type (P = Passport)
Line 2: AB1234567<NLD7801015M2512315<<<<<<<<<<<<<<<8
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ Composite check digit
│ │ │ │ │ └───────────────── Optional data
│ │ │ │ └──────────────────────── Expiry date (YYMMDD) + check
│ │ │ └─────────────────────────── Gender (M/F/<)
│ │ └────────────────────────────────── DOB (YYMMDD) + check
│ └────────────────────────────────────── Nationality
└─────────────────────────────────────────────── Document number + check
Tracks MRZ field detection across multiple camera frames for confidence.
Features:
- Multi-frame validation (requires 3+ consistent matches)
- Separate tracking for each field (doc number, DOB, expiry)
- Real-time status indicators (missing/pending/validated)
- History management (last 5 frames)
Usage:
let tracker = MRZTracker()
// Add detection from each camera frame
tracker.addDetection(
documentNumber: "AB1234567",
dateOfBirth: "780101",
dateOfExpiry: "251231"
)
// Check field status
switch tracker.documentNumberStatus {
case .missing:
print("Not yet detected")
case .pending:
print("Detected, waiting for confirmation...")
case .validated:
print("✓ Confirmed!")
}
// Get validated values (only when confident)
if tracker.allFieldsValidated {
let docNumber = tracker.validatedDocumentNumber
let dob = tracker.validatedDateOfBirth
let expiry = tracker.validatedDateOfExpiry
}Extracts facial biometric data from ePassport Data Group 2.
Features:
- ISO/IEC 19794-5 format parsing
- TLV (Tag-Length-Value) structure parsing
- Biometric data extraction
- Face image data isolation
Usage:
let parser = DG2Parser(data: dg2Data)
if let imageData = parser.extractImageData() {
// imageData contains the raw face image (JPEG or JPEG2000)
let image = ImageDecoderService.shared.decodeJPEG2000(imageData)
}DG2 Structure:
DG2 (Data Group 2)
├── 0x75 - Outer wrapper
├── 0x7F61 - Biometric Information Template
│ ├── 0x02 - Number of instances
│ └── 0x7F60 - Biometric Information Group
│ ├── 0xA1 - Biometric Header (skip)
│ └── 0x5F2E/0x7F2E - Biometric data block
│ └── ISO 19794-5 format
│ ├── Format ID (4 bytes)
│ ├── Version (4 bytes)
│ ├── Record length (4 bytes)
│ ├── Face image metadata
│ └── Image data (JPEG/JPEG2000)
Decodes JPEG2000 and other image formats from passport chips.
Features:
- JPEG2000 decoding (via ImageIO)
- JPEG detection and decoding
- Format auto-detection
- Fallback handling
Usage:
let decoder = ImageDecoderService.shared
// Decode JPEG2000
if let image = decoder.decodeJPEG2000(jp2Data) {
photoImageView.image = image
}
// Check format
if decoder.isJPEG2000(imageData) {
print("JPEG2000 format detected")
}
// Convert to JPEG
if let jpegData = decoder.convertToJPEG(imageData, quality: 0.9) {
// Upload jpegData
}The SDK uses Vision framework's text recognition to scan MRZ from camera feed.
Implementation Approach:
import Vision
import AVFoundation
class MRZScannerViewController: UIViewController {
let tracker = MRZTracker()
let textRecognizer = VNRecognizeTextRequest()
func processCameraFrame(_ pixelBuffer: CVPixelBuffer) {
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try? handler.perform([textRecognizer])
guard let results = textRecognizer.results else { return }
// Extract all text lines
let lines = results.compactMap { $0.topCandidates(1).first?.string }
// Look for MRZ pattern (line 1 + line 2)
for i in 0..<lines.count - 1 {
let line1 = lines[i]
let line2 = lines[i + 1]
if MRZParser.isPassportMRZLine1(line1) &&
MRZParser.looksLikeMRZLine2(line2) {
let parsed = MRZParser.parseLine2(line2)
tracker.addDetection(
documentNumber: parsed["documentNumber"],
dateOfBirth: parsed["dateOfBirth"],
dateOfExpiry: parsed["dateOfExpiry"]
)
if tracker.allFieldsValidated {
onMRZDetected(tracker)
}
break
}
}
}
}Best Practices:
- Process frames at 5 FPS (200ms intervals) to balance performance
- Use high resolution preset for better OCR accuracy
- Show real-time field status indicators (✓/⏳/○)
- Provide visual guide for MRZ area alignment
- Auto-proceed when all fields validated
Uses MRZ data (document number + DOB + expiry) to generate access key.
let credentials = OkIDPassportCredentials(
documentNumber: "AB1234567",
dateOfBirth: Date(timeIntervalSince1970: 252460800), // 1978-01-01
dateOfExpiry: Date(timeIntervalSince1970: 1798761600) // 2027-01-01
)
// SDK uses BAC if no CAN providedUses 6-digit Card Access Number (printed on passport front).
let credentials = OkIDPassportCredentials(
documentNumber: "AB1234567",
dateOfBirth: Date(timeIntervalSince1970: 252460800),
dateOfExpiry: Date(timeIntervalSince1970: 1798761600),
can: "123456" // 6-digit CAN
)
// SDK automatically uses PACE if CAN provided1. Initialize NFC Session
└─▶ Poll for ISO 14443 tag
2. Read EF.CardAccess (if available)
└─▶ Determine PACE support
3. Establish Secure Session
├─▶ PACE (if CAN or CardAccess available)
└─▶ BAC (fallback)
4. Read EF.COM
└─▶ Get list of available Data Groups
5. Read Data Groups
├─▶ DG1: MRZ (personal info)
├─▶ DG2: Face image
├─▶ DG11: Additional personal details
├─▶ DG12: Additional document details
├─▶ DG15: Active Auth public key
└─▶ EF.SOD: Document security
6. Extract and Parse
├─▶ Parse MRZ from DG1
├─▶ Extract photo from DG2
└─▶ Validate with SOD
7. Return Results
└─▶ OkIDPassportData
struct NFCReadingProgress {
let state: NFCReadingState // idle, detecting, connecting, etc.
let message: String // User-friendly message
let progress: Double // 0.0 to 1.0
}
// States:
// .detecting → "Hold passport near device..." (0%)
// .connecting → "Connecting to passport..." (10%)
// .readingCardAccess → "Reading card access..." (20%)
// .establishingSession→ "Establishing session..." (30%)
// .readingCOM → "Reading COM..." (40%)
// .readingDataGroups → "Reading DG1..." (50-99%)
// .completed → "Reading complete!" (100%)
// .error → Error messagepublic struct OkIDPassportCredentials {
let documentNumber: String // e.g., "AB1234567"
let dateOfBirth: Date
let dateOfExpiry: Date
let can: String? // Optional 6-digit CAN
var hasCAN: Bool // true if CAN is 6 digits
var dobFormatted: String // YYMMDD format
var expiryFormatted: String // YYMMDD format
func validate() -> Bool
}public struct OkIDPersonalInfo {
let documentType: String? // "P" for passport
let issuingState: String? // 3-letter country code
let documentNumber: String?
let lastName: String?
let firstName: String?
let nationality: String?
let dateOfBirth: Date?
let gender: String? // "M", "F", or "<"
let dateOfExpiry: Date?
let optionalData1: String?
let optionalData2: String?
var fullName: String // "FirstName LastName"
}public struct OkIDPassportData {
let personalInfo: OkIDPersonalInfo?
let photo: Data? // JPEG image data
let dataGroupsRead: [String] // ["DG1", "DG2", "DG11", ...]
let readAt: Date
let additionalInfo: [String: Any]? // Extra metadata
}import OkIDVerificationSDK
class PassportScanViewController: UIViewController {
func startNFCScan() {
// Option 1: Manual entry
let credentials = OkIDPassportCredentials(
documentNumber: "AB1234567",
dateOfBirth: parseDate("01.01.1978"),
dateOfExpiry: parseDate("01.01.2027"),
can: "123456" // Optional
)
readPassport(credentials: credentials)
// Option 2: From MRZ scan
showMRZScanner { scannedCredentials in
self.readPassport(credentials: scannedCredentials)
}
}
func readPassport(credentials: OkIDPassportCredentials) {
guard NFCReader.isAvailable else {
showAlert("NFC not available on this device")
return
}
let reader = NFCPassportReader()
reader.readPassport(
credentials: credentials,
progress: { progress in
self.updateProgress(progress)
},
completion: { result in
switch result {
case .success(let passportData):
self.handleSuccess(passportData)
case .failure(let error):
self.handleError(error)
}
}
)
}
func handleSuccess(_ data: OkIDPassportData) {
print("Name: \(data.personalInfo?.fullName ?? "N/A")")
print("Nationality: \(data.personalInfo?.nationality ?? "N/A")")
print("Data groups: \(data.dataGroupsRead.joined(separator: ", "))")
if let photoData = data.photo,
let image = UIImage(data: photoData) {
photoImageView.image = image
}
}
}class MRZScannerController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
let tracker = MRZTracker()
var captureSession: AVCaptureSession?
func setupCamera() {
captureSession = AVCaptureSession()
captureSession?.sessionPreset = .high
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global())
captureSession?.addOutput(videoOutput)
captureSession?.startRunning()
}
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let request = VNRecognizeTextRequest { request, error in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let lines = observations.compactMap { $0.topCandidates(1).first?.string }
self.processLines(lines)
}
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try? handler.perform([request])
}
func processLines(_ lines: [String]) {
for i in 0..<lines.count - 1 {
if MRZParser.isPassportMRZLine1(lines[i]) &&
MRZParser.looksLikeMRZLine2(lines[i + 1]) {
let parsed = MRZParser.parseLine2(lines[i + 1])
tracker.addDetection(
documentNumber: parsed["documentNumber"],
dateOfBirth: parsed["dateOfBirth"],
dateOfExpiry: parsed["dateOfExpiry"]
)
updateUI()
if tracker.allFieldsValidated {
onMRZComplete()
}
}
}
}
}Problem: NFC reading fails immediately
Solutions:
- Check device support (iPhone 7+ required)
- Verify NFC is enabled in Settings
- Request NFC permissions in Info.plist
- Check for iOS version compatibility (iOS 13+)
import CoreNFC
if NFCReaderSession.readingAvailable {
// NFC is available
} else {
showAlert("NFC not supported on this device")
}Problem: "Authentication failed" error during NFC reading
Solutions:
- Verify document number (no spaces/dashes)
- Check date formats (DDMMYY)
- Ensure dates match passport exactly
- Try CAN-based PACE if available
- Check passport is NFC-enabled (chip symbol)
// Correct format
let credentials = OkIDPassportCredentials(
documentNumber: "AB1234567", // No spaces
dateOfBirth: Date(...), // Exact date
dateOfExpiry: Date(...),
can: "123456" // Try with CAN
)Problem: Camera can't detect MRZ text
Solutions:
- Improve lighting conditions
- Hold camera steady over MRZ area
- Use high resolution camera preset
- Ensure MRZ area is clean/unobstructed
- Check OCR language settings (Latin script)
// Optimize OCR
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
request.recognitionLanguages = ["en"]
request.usesLanguageCorrection = trueProblem: Can't extract photo from passport
Solutions:
- Check Data Group 2 is available in EF.COM
- Verify DG2 was read successfully
- Handle both JPEG and JPEG2000 formats
- Check for DG2 parsing errors
if let imageData = dg2Parser.extractImageData() {
// Check format
if ImageDecoderService.shared.isJPEG2000(imageData) {
// Decode JPEG2000
image = ImageDecoderService.shared.decodeJPEG2000(imageData)
} else {
// Already JPEG
image = UIImage(data: imageData)
}
}Problem: "Tag connection lost" error
Solutions:
- Hold passport steady against device
- Ensure NFC antenna position (usually top back of iPhone)
- Remove phone case if metallic
- Don't move passport during reading
- Avoid electromagnetic interference
<key>NFCReaderUsageDescription</key>
<string>NFC access is required to read your passport chip data for identity verification.</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A0000002471001</string>
</array>- Enable "Near Field Communication Tag Reading" in Xcode capabilities
- Add NFC entitlement to App ID in Apple Developer Portal
- Device: iPhone 7 or newer
- iOS: 13.0+
- Xcode: 12.0+
- Swift: 5.3+
-
Never log sensitive data:
// ❌ Bad print("Document number: \(credentials.documentNumber)") // ✓ Good print("Reading passport data...")
-
Validate all inputs:
guard credentials.validate() else { throw ValidationError.invalidCredentials }
-
Use secure storage for credentials:
// Store in Keychain, not UserDefaults KeychainService.save(credentials, for: "passport_creds")
- Throttle camera frame processing (5 FPS max)
- Use background threads for OCR
- Cache MRZ tracker results
- Preload Vision models
- Show clear instructions (where to place passport)
- Provide real-time feedback (field status indicators)
- Display progress during NFC reading (percentage + message)
- Handle errors gracefully (retry options)
- Vibrate/sound on success
The OkID iOS SDK provides a complete NFC passport reading solution with:
✅ MRZ OCR Scanning: Automatic credential extraction with error correction
✅ NFC Reading: PACE and BAC support for secure chip reading
✅ Biometric Extraction: DG2 parsing with JPEG/JPEG2000 decoding
✅ Cross-frame Validation: Confidence-based field tracking
✅ Progress Tracking: Real-time user feedback
✅ Error Handling: Comprehensive error types and recovery
For implementation support: support@okid.com
Documentation: https://docs.okid.com/nfc