taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }