TagEmulation.swift (7515B)
1 // 2 // TagEmulation.swift 3 // HCEtest 4 // 5 // Created by Marc Stibane on 2024-09-01. 6 // 7 8 import CoreNFC 9 import os.log 10 11 @available(iOS 17.7, *) 12 @MainActor 13 class TagEmulation: ObservableObject { 14 public static let shared = TagEmulation() 15 @Published var canUseHCE: Bool = false 16 17 nonisolated let logger = Logger(subsystem: "net.taler.gnu", category: "TagEmulation") 18 private var cardSession: CardSession? = nil 19 private var presentmentIntent: NFCPresentmentIntentAssertion? = nil 20 21 init() { 22 if NFCReaderSession.readingAvailable { 23 if CardSession.isSupported { 24 Task { 25 if await CardSession.isEligible { 26 logger.info("CardSession is eligible") 27 canUseHCE = true 28 } else { logger.error("❗️CardSession is not eligible"); canUseHCE = false } 29 } 30 } else { logger.error("❗️CardSession is not supported"); canUseHCE = false } 31 } else { logger.error("❗️NFCReaderSession is not available"); canUseHCE = false } 32 } 33 34 func emulateTag(_ emulatedData: String) { 35 Task() { 36 let result = await startEmulation(emulatedData) 37 if result { 38 logger.info("Emulation was successful") 39 } else { 40 logger.warning("❗️Emulation was not successful") 41 } 42 } 43 } 44 45 private func startEmulation(_ emulatedData: String) async -> Bool { 46 // Hold a presentment intent assertion reference to prevent the 47 // default contactless app from launching. In a real app, monitor 48 // presentmentIntent.isValid to ensure the assertion remains active. 49 var result = false 50 51 guard emulatedData.count < 8192 else { return false } 52 do { 53 if presentmentIntent == nil { 54 presentmentIntent = try await NFCPresentmentIntentAssertion.acquire() 55 /// The presentment intent assertion expires if any of the following occur: 56 /// • The presentment intent assertion object deinitializes. 57 /// • Your app goes into the background. 58 /// • 15 seconds elapse. 59 /// After the presentment intent assertion expires, you must wait through a 15-second cool-down period before you can acquire a new instance. 60 logger.info("NFCPresentmentIntentAssertion acquired") 61 } else { 62 logger.info("NFCPresentmentIntentAssertion exists") 63 } 64 if cardSession == nil { 65 cardSession = try await CardSession() 66 logger.info("CardSession launched") 67 } else { 68 logger.info("❗️Yikes! CardSession exists") 69 } 70 if let cardSession { 71 logger.info("cardSession.startEmulation (without waiting for reader)") 72 try await cardSession.startEmulation() 73 74 logger.info("starting eventStream") 75 try await eventStream(cardSession, data: emulatedData) 76 logger.info("eventStream finished") 77 78 try? await Task.sleep(nanoseconds: 1_000_000_000 * 3) 79 80 logger.info("cardSession.invalidate") 81 await cardSession.invalidate() 82 result = true 83 } 84 cardSession = nil 85 } catch let error { 86 // Handle failure to acquire NFC presentment intent assertion or card session. 87 logger.error("❗️NFCPresentmentIntentAssertion not possible: \(error.localizedDescription)") 88 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 89 self.presentmentIntent = nil /// Release presentment intent assertion. 90 } 91 return false 92 } 93 return result 94 } 95 96 func killEmulation() { 97 Task() { 98 if let session = cardSession { 99 await session.invalidate() 100 cardSession = nil 101 } 102 logger.info("NFCPresentmentIntentAssertion released") 103 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 104 if self.cardSession == nil { // don't release if there is a new session 105 self.presentmentIntent = nil // release presentment intent assertion 106 } 107 } 108 } 109 } 110 111 private func eventStream(_ mySession: CardSession, data emulatedData: String) async throws { 112 let apdu = try APDU(emulatedData) 113 // Iterate over events as the card session produces them. 114 for try await event in mySession.eventStream { 115 // if presentmentIntent?.isValid ?? false { 116 switch event { 117 case .sessionStarted: 118 #if DEBUG 119 let message = "sessionStarted" 120 mySession.alertMessage = message 121 logger.info("\(message)") 122 #endif 123 break 124 125 case .readerDetected: 126 /// Start card emulation on first detection of an external reader. 127 logger.info("readerDetected") 128 // logger.info("cardSession.startEmulation") 129 // try await mySession.startEmulation() 130 131 case .readerDeselected: 132 /// Stop emulation on first notification of RF link loss. 133 logger.info("❗️readerDeselected. cardSession.stopEmulation") 134 await mySession.stopEmulation(status: .success) 135 return 136 137 case .received(let cardAPDU): 138 do { 139 /// Call handler to process received input and produce a response. 140 let responseAPDU = apdu.processAPDU(cardAPDU.payload) 141 142 logger.info("sending back data: \(responseAPDU.hexEncodedString())") 143 try await cardAPDU.respond(response: responseAPDU) 144 } catch { 145 /// Handle the error from respond(response:). If the error is 146 /// CardSession.Error.transmissionError, then retry by calling 147 /// CardSession.APDU.respond(response:) again. 148 logger.error("❗️Error while responding: \(error)") 149 } 150 151 case .sessionInvalidated(reason: _): 152 #if DEBUG 153 mySession.alertMessage = "Ending communication with card reader." 154 logger.info("❗️cardSession invalidation") 155 #endif 156 /// Handle the reason for session invalidation. 157 await mySession.stopEmulation(status: .failure) 158 return 159 160 default: 161 #if DEBUG 162 let message = "Unknown event from card reader." 163 mySession.alertMessage = message 164 logger.info("❗️\(message)") 165 #endif 166 break 167 } 168 // } else { 169 // logger.error("❗️presentmentIntent is not valid") 170 // await mySession.stopEmulation(status: .failure) 171 // presentmentIntent = nil /// Release presentment intent assertion. 172 // // TODO: stop eventStream? 173 // } 174 } // cardSession.eventStream 175 } 176 }