TagEmulation.swift (7449B)
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 do { 52 if presentmentIntent == nil { 53 presentmentIntent = try await NFCPresentmentIntentAssertion.acquire() 54 /// The presentment intent assertion expires if any of the following occur: 55 /// • The presentment intent assertion object deinitializes. 56 /// • Your app goes into the background. 57 /// • 15 seconds elapse. 58 /// After the presentment intent assertion expires, you must wait through a 15-second cool-down period before you can acquire a new instance. 59 logger.info("NFCPresentmentIntentAssertion acquired") 60 } else { 61 logger.info("NFCPresentmentIntentAssertion exists") 62 } 63 if cardSession == nil { 64 cardSession = try await CardSession() 65 logger.info("CardSession launched") 66 } else { 67 logger.info("❗️Yikes! CardSession exists") 68 } 69 if let cardSession { 70 logger.info("cardSession.startEmulation (without waiting for reader)") 71 try await cardSession.startEmulation() 72 73 logger.info("starting eventStream") 74 try await eventStream(cardSession, data: emulatedData) 75 logger.info("eventStream finished") 76 77 try? await Task.sleep(nanoseconds: 1_000_000_000 * 3) 78 79 logger.info("cardSession.invalidate") 80 await cardSession.invalidate() 81 result = true 82 } 83 cardSession = nil 84 } catch let error { 85 // Handle failure to acquire NFC presentment intent assertion or card session. 86 logger.error("❗️NFCPresentmentIntentAssertion not possible: \(error.localizedDescription)") 87 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 88 self.presentmentIntent = nil /// Release presentment intent assertion. 89 } 90 return false 91 } 92 return result 93 } 94 95 func killEmulation() { 96 Task() { 97 if let session = cardSession { 98 await session.invalidate() 99 cardSession = nil 100 } 101 logger.info("NFCPresentmentIntentAssertion released") 102 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 103 if self.cardSession == nil { // don't release if there is a new session 104 self.presentmentIntent = nil // release presentment intent assertion 105 } 106 } 107 } 108 } 109 110 private func eventStream(_ mySession: CardSession, data emulatedData: String) async throws { 111 let apdu = APDU(emulatedData) 112 // Iterate over events as the card session produces them. 113 for try await event in mySession.eventStream { 114 // if presentmentIntent?.isValid ?? false { 115 switch event { 116 case .sessionStarted: 117 #if DEBUG 118 let message = "sessionStarted" 119 mySession.alertMessage = message 120 logger.info("\(message)") 121 #endif 122 break 123 124 case .readerDetected: 125 /// Start card emulation on first detection of an external reader. 126 logger.info("readerDetected") 127 // logger.info("cardSession.startEmulation") 128 // try await mySession.startEmulation() 129 130 case .readerDeselected: 131 /// Stop emulation on first notification of RF link loss. 132 logger.info("❗️readerDeselected. cardSession.stopEmulation") 133 await mySession.stopEmulation(status: .success) 134 return 135 136 case .received(let cardAPDU): 137 do { 138 /// Call handler to process received input and produce a response. 139 let responseAPDU = apdu.processAPDU(cardAPDU.payload) 140 141 logger.info("sending back data: \(responseAPDU.hexEncodedString())") 142 try await cardAPDU.respond(response: responseAPDU) 143 } catch { 144 /// Handle the error from respond(response:). If the error is 145 /// CardSession.Error.transmissionError, then retry by calling 146 /// CardSession.APDU.respond(response:) again. 147 logger.error("❗️Error while responding: \(error)") 148 } 149 150 case .sessionInvalidated(reason: _): 151 #if DEBUG 152 mySession.alertMessage = "Ending communication with card reader." 153 logger.info("❗️cardSession invalidation") 154 #endif 155 /// Handle the reason for session invalidation. 156 await mySession.stopEmulation(status: .failure) 157 return 158 159 default: 160 #if DEBUG 161 let message = "Unknown event from card reader." 162 mySession.alertMessage = message 163 logger.info("❗️\(message)") 164 #endif 165 break 166 } 167 // } else { 168 // logger.error("❗️presentmentIntent is not valid") 169 // await mySession.stopEmulation(status: .failure) 170 // presentmentIntent = nil /// Release presentment intent assertion. 171 // // TODO: stop eventStream? 172 // } 173 } // cardSession.eventStream 174 } 175 }