commit 763554efd0542006e5a5c999eff88385995da421
parent 0776b681276e4040aa1c586e9b23c5529cc857ac
Author: Marc Stibane <marc@taler.net>
Date: Fri, 30 May 2025 20:38:14 +0200
HCE
Diffstat:
5 files changed, 489 insertions(+), 0 deletions(-)
diff --git a/GNU_Taler.entitlements b/GNU_Taler.entitlements
@@ -4,6 +4,12 @@
<dict>
<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>
+ <key>com.apple.developer.nfc.hce</key>
+ <true/>
+ <key>com.apple.developer.nfc.hce.iso7816.select-identifier-prefixes</key>
+ <array>
+ <string>D2760000850101</string>
+ </array>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
diff --git a/TalerWallet1/Helper/Data+fromUInt8Array.swift b/TalerWallet1/Helper/Data+fromUInt8Array.swift
@@ -0,0 +1,25 @@
+/*
+ * This file is part of GNU Taler, ©2022-24 Taler Systems S.A.
+ * See LICENSE.md
+ */
+/**
+ * @author Marc Stibane
+ */
+import Foundation
+
+extension Data {
+ init(fromUInt8Array inValues: [UInt8]) {
+ let values = inValues
+ self.init(bytes: values, count: values.count)
+ }
+
+ struct HexEncodingOptions: OptionSet {
+ let rawValue: Int
+ static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
+ }
+
+ func hexEncodedString(options: HexEncodingOptions = []) -> String {
+ let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
+ return self.map { String(format: format, $0) }.joined()
+ }
+}
diff --git a/TalerWallet1/Helper/TagEmulation.swift b/TalerWallet1/Helper/TagEmulation.swift
@@ -0,0 +1,154 @@
+//
+// TagEmulation.swift
+// HCEtest
+//
+// Created by Marc Stibane on 2024-09-01.
+//
+
+import CoreNFC
+import os.log
+
+@available(iOS 18.3, *)
+@MainActor
+class TagEmulation: ObservableObject {
+
+ @Published var canUseHCE: Bool = false
+
+ nonisolated let logger = Logger(subsystem: "net.taler.gnu", category: "TagEmulation")
+ private var cardSession: CardSession? = nil
+ private var presentmentIntent: NFCPresentmentIntentAssertion? = nil
+
+ init() {
+ if NFCReaderSession.readingAvailable {
+ if CardSession.isSupported {
+ Task {
+ if await CardSession.isEligible {
+ logger.info("CardSession is eligible")
+ canUseHCE = true
+ } else { logger.error("❗️CardSession is not eligible"); canUseHCE = false }
+ }
+ } else { logger.error("❗️CardSession is not supported"); canUseHCE = false }
+ } else { logger.error("❗️NFCReaderSession is not available"); canUseHCE = false }
+ }
+
+ func emulateTag(_ emulatedData: String) {
+ Task() {
+ let result = await startEmulation(emulatedData)
+ if result {
+ logger.info("Emulation was successful")
+ } else {
+ logger.warning("❗️Emulation was not successful")
+ }
+ }
+ }
+
+ private func startEmulation(_ emulatedData: String) async -> Bool {
+ // Hold a presentment intent assertion reference to prevent the
+ // default contactless app from launching. In a real app, monitor
+ // presentmentIntent.isValid to ensure the assertion remains active.
+ var result = false
+
+ do {
+ if presentmentIntent == nil {
+ presentmentIntent = try await NFCPresentmentIntentAssertion.acquire()
+ logger.info("NFCPresentmentIntentAssertion acquired")
+ } else {
+ logger.info("NFCPresentmentIntentAssertion exists")
+ }
+ if cardSession == nil {
+ cardSession = try await CardSession()
+ logger.info("CardSession launched")
+ } else {
+ logger.info("❗️Yikes! CardSession exists")
+ }
+ if let cardSession {
+ logger.info("cardSession.startEmulation (without waiting for reader)")
+ try await cardSession.startEmulation()
+
+ logger.info("starting eventStream")
+ try await eventStream(cardSession, data: emulatedData)
+ logger.info("eventStream finished")
+ result = true
+ }
+ } catch let error {
+ // Handle failure to acquire NFC presentment intent assertion or card session.
+ logger.error("❗️NFCPresentmentIntentAssertion not possible: \(error.localizedDescription)")
+ presentmentIntent = nil /// Release presentment intent assertion.
+ return false
+ }
+ return result
+ }
+
+ func killEmulation() {
+ Task() {
+ if let session = cardSession {
+ if await session.isEmulationInProgress {
+ logger.info("cardSession.invalidate")
+ }
+ await session.invalidate()
+ cardSession = nil
+ }
+ logger.info("NFCPresentmentIntentAssertion released")
+ presentmentIntent = nil /// Release presentment intent assertion
+ }
+ }
+
+ private func eventStream(_ mySession: CardSession, data emulatedData: String) async throws {
+ let apdu = APDU(emulatedData)
+ // Iterate over events as the card session produces them.
+ for try await event in mySession.eventStream {
+// if presentmentIntent?.isValid ?? false {
+ switch event {
+ case .sessionStarted:
+ let message = "sessionStarted"
+ mySession.alertMessage = message
+ logger.info("\(message)")
+ break
+
+ case .readerDetected:
+ /// Start card emulation on first detection of an external reader.
+ logger.info("readerDetected")
+// logger.info("cardSession.startEmulation")
+// try await mySession.startEmulation()
+
+ case .readerDeselected:
+ /// Stop emulation on first notification of RF link loss.
+ logger.info("❗️readerDeselected. cardSession.stopEmulation")
+ await mySession.stopEmulation(status: .success)
+ return
+
+ case .received(let cardAPDU):
+ do {
+ /// Call handler to process received input and produce a response.
+ let responseAPDU = apdu.processAPDU(cardAPDU.payload)
+
+ logger.info("sending back data: \(responseAPDU.hexEncodedString())")
+ try await cardAPDU.respond(response: responseAPDU)
+ } catch {
+ /// Handle the error from respond(response:). If the error is
+ /// CardSession.Error.transmissionError, then retry by calling
+ /// CardSession.APDU.respond(response:) again.
+ logger.error("❗️Error while responding: \(error)")
+ }
+
+ case .sessionInvalidated(reason: _):
+ mySession.alertMessage = "Ending communication with card reader."
+ logger.info("❗️cardSession invalidation")
+ /// Handle the reason for session invalidation.
+ await mySession.stopEmulation(status: .failure)
+ return
+
+ default:
+ let message = "Unknown event from card reader."
+ mySession.alertMessage = message
+ logger.info("❗️\(message)")
+ }
+// } else {
+// logger.error("❗️presentmentIntent is not valid")
+// await mySession.stopEmulation(status: .failure)
+// presentmentIntent = nil /// Release presentment intent assertion.
+// // TODO: stop eventStream?
+// }
+ } // cardSession.eventStream
+ }
+}
diff --git a/TalerWallet1/Helper/apdu.swift b/TalerWallet1/Helper/apdu.swift
@@ -0,0 +1,298 @@
+//
+// apdu.swift
+// HCEtest
+//
+// Created by Marc Stibane on 2024-07-27.
+//
+
+import Foundation
+import os.log
+
+@MainActor
+class APDU {
+ let talerUri: String
+ nonisolated let logger = Logger(subsystem: "net.taler.gnu", category: "APDU")
+ private var readCapabilityContainerCheck = false
+
+ init(_ talerUri: String) {
+ self.talerUri = talerUri
+ }
+
+ private var APDU_SELECT: Data {
+ Data(fromUInt8Array: [
+ 0x00, // CLA - Class - Class of instruction
+ 0xA4, // INS - Instruction - Instruction code
+ 0x04, // P1 - Parameter 1 - Instruction parameter 1
+ 0x00, // P2 - Parameter 2 - Instruction parameter 2
+ 0x07, // Lc field - Number of bytes present in the data field of the command
+ 0xD2, // NDEF Tag Application name
+ 0x76,
+ 0x00,
+ 0x00,
+ 0x85,
+ 0x01,
+ 0x01,
+ 0x00, // Le field - Maximum number of bytes expected in the data field of the response to the command
+ ])
+ } // 00a4 0400 07 d2760000850101 00
+
+ private var CAPABILITY_CONTAINER_OK: Data {
+ Data(fromUInt8Array: [
+ 0x00, // CLA - Class - Class of instruction
+ 0xA4, // INS - Instruction - Instruction code
+ 0x00, // P1 - Parameter 1 - Instruction parameter 1
+ 0x0C, // P2 - Parameter 2 - Instruction parameter 2
+ 0x02, // Lc field - Number of bytes present in the data field of the command
+ 0xE1, // file identifier of the CC file
+ 0x03,
+ ])
+ } // 00a4 000c 02 e103
+
+ private var READ_CAPABILITY_CONTAINER: Data {
+ Data(fromUInt8Array: [
+ 0x00, // CLA - Class - Class of instruction
+ 0xB0, // INS - Instruction - Instruction code
+ 0x00, // P1 - Parameter 1 - Instruction parameter 1
+ 0x00, // P2 - Parameter 2 - Instruction parameter 2
+ 0x0F, // Lc field - Number of bytes present in the data field of the command
+ ])
+ } // 00b0 0000 0f
+
+// private var READ_CAPABILITY_CONTAINER_RESPONSE: Data {
+// Data(fromUInt8Array: [
+// 0x00, 0x11, // CCLEN length of the CC file
+// 0x20, // Mapping Version 2.0
+// 0xFF, 0xFF, // MLe maximum
+// 0xFF, 0xFF, // MLc maximum
+// 0x04, // T field of the NDEF File Control TLV
+// 0x06, // L field of the NDEF File Control TLV
+// 0xE1, 0x04, // File Identifier of NDEF file
+// 0xFF, 0xFE, // Maximum NDEF file size of 65534 bytes
+// 0x00, // Read access without any security
+// 0xFF, // Write access without any security
+// 0x90, 0x00, // A_OKAY
+// ])
+// } // 0011 20ffffffff 0406 e104 ffff 00ff 9000
+
+ private var READ_CAPABILITY_CONTAINER_RESPONSE: Data {
+ Data(fromUInt8Array: [
+ 0x00, 0x0f, // CCLEN length of the CC file
+ 0x20, // Mapping Version 2.0
+ 0x00, 0x3B, // MLe maximum
+ 0x00, 0x34, // MLc maximum
+ 0x04, // T field of the NDEF File Control TLV
+ 0x06, // L field of the NDEF File Control TLV
+ 0xE1, 0x04, // File Identifier of NDEF file
+ 0x00, 0xFF, // Maximum NDEF file size of 65534 bytes
+ 0x00, // Read access without any security
+ 0xFF, // Write access without any security
+ 0x90, 0x00, // A_OKAY
+ ])
+ } //000f 20 003b 0034 04 06 e104 00ff 00ff9000
+
+
+ private var NDEF_SELECT_OK: Data {
+ Data(fromUInt8Array: [
+ 0x00, // CLA - Class - Class of instruction
+ 0xA4, // Instruction byte (INS) for Select command
+ 0x00, // Parameter byte (P1), select by identifier
+ 0x0C, // Parameter byte (P2), select by identifier
+ 0x02, // Lc field - Number of bytes present in the data field of the command
+ 0xE1,
+ 0x04, // file identifier of the NDEF file retrieved from the CC file
+ ])
+ } // 00a4 000c 02 e104
+
+ private var NDEF_READ_BINARY_NLEN: Data {
+ Data(fromUInt8Array: [
+ 0x00, // Class byte (CLA)
+ 0xB0, // Instruction byte (INS) for ReadBinary command
+ 0x00,
+ 0x00, // Parameter byte (P1, P2), offset inside the CC file
+ 0x02, // Le field
+ ])
+ } // 00b0 0000 02
+
+ private var NDEF_READ_BINARY: Data {
+ Data(fromUInt8Array: [
+ 0x00, // Class byte (CLA)
+ 0xB0, // Instruction byte (INS) for ReadBinary command
+ ])
+ } // 00b0
+// private let NDEF_READ_BINARY_DATA = Data(fromUInt8Array: NDEF_READ_BINARY)
+
+ private var NDEF_RECORD_HEADER_SHORT: Data {
+ Data(fromUInt8Array: [
+ 0xD1, // 1101 + 0001 = MessageBegin, MessageEnd, ShortRecord + Format "Well-Known" (but no ID Length)
+ 0x01, // TypeLength
+ // 0x00, // Payload Length 1 byte only since ShortRecord is true
+ // no ID Length byte since the IDlength flag is false
+ // 0x55, // Payload Type = “U” for URI, 1 byte as specified in the TypeLength field
+ // since we have no ID length, there is no Payload ID
+ ])
+ } // d101
+
+ private var NDEF_RECORD_HEADER_LONG: Data {
+ Data(fromUInt8Array: [
+ 0xD1, // 1100 + 0001 = MessageBegin, MessageEnd + Format "Well-Known" (but no ID Length and no ShortRecord)
+ 0x01, // TypeLength
+ // 0x0000, 0x0000, // Payload Length 4 bytes since ShortRecord is false
+ // no ID Length byte since the IDlength flag is false
+ // 0x55, // Payload Type = “U” for URI, 1 byte as specified in the TypeLength field
+ // since we have no ID length, there is no Payload ID
+ ])
+ } // c101
+
+ private var A_OKAY_ARRAY: [UInt8] {
+ [
+ 0x90, // SW1 Status byte 1 - Command processing status
+ 0x00, // SW2 Status byte 2 - Command processing qualifier
+ ]
+ } // 9000 = OK
+ private var A_OKAY: Data {
+ Data(fromUInt8Array: A_OKAY_ARRAY)
+ }
+
+ private var A_ERROR: Data {
+ Data(fromUInt8Array: [
+ 0x6A, // SW1 Status byte 1 - Command processing status
+ 0x82, // SW2 Status byte 2 - Command processing qualifier
+ ])
+ } // 6A82 = File not found
+
+ private var APPLICATION_ERROR: Data {
+ Data(fromUInt8Array: [
+ 0x6A, // SW1 Status byte 1 - Command processing status
+ 0x88, // SW2 Status byte 2 - Command processing qualifier
+ ])
+ } // 6A88 = no application ID
+
+ private var GET_VERSION: Data {
+ Data(fromUInt8Array: [
+ 0x90, // CLA - Class - Class of instruction
+ 0x60, // INS - Instruction - Instruction code
+ 0x00, // P1 - Parameter 1 - Instruction parameter 1
+ 0x00, // P2 - Parameter 2 - Instruction parameter 2
+ 0x00, // Lc field - Number of bytes present in the data field of the command
+ ])
+ } // 9060 0000 00
+
+ private var GET_VERSION_RESPONSE: Data {
+ Data(fromUInt8Array: [
+ 0x00, // fixed header
+ 0x04, // vendor ID (04 = NXP Semiconductors)
+ 0x04, // product type (04 = NTAG)
+ 0x02, // product subtype (02 = NTAG215)
+ 0x01, // major product version
+ 0x00, // minor product version
+ 0x11, // storage size
+ 0x03, // protocol type: ISO/IEC 14443-3 compliant
+ 0x90, 0x00, // A_OKAY
+ ])
+ } //
+
+ private var MORE_INFO: Data {
+ Data(fromUInt8Array: [
+ 0x90, // CLA - Class - Class of instruction
+ 0xAF, // INS - Instruction - Instruction code
+ 0x00, // P1 - Parameter 1 - Instruction parameter 1
+ 0x00, // P2 - Parameter 2 - Instruction parameter 2
+ 0x00, // Lc field - Number of bytes present in the data field of the command
+ ])
+ } // 90af 0000 00
+
+ /// process the received data and return a response as Data.
+ func processAPDU(_ commandApdu: Data) -> Data {
+ /// The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow"
+ /// in the NFC Forum specification
+ logger.info("incoming commandApdu: \(commandApdu.hexEncodedString())")
+
+ /// First command: NDEF Tag Application select
+ if APDU_SELECT == commandApdu {
+ logger.info("APDU_SELECT triggered. Our Response: A_OKAY")
+ return A_OKAY
+ } // 00a4040007d276000085010100
+
+ /// Second command: Capability Container select
+ if CAPABILITY_CONTAINER_OK == commandApdu {
+ logger.info("CAPABILITY_CONTAINER_OK triggered. Our Response: A_OKAY")
+ return A_OKAY
+ } // 00a4000c02e103
+
+ /// Third command: ReadBinary data from CC file
+ if READ_CAPABILITY_CONTAINER == commandApdu && !readCapabilityContainerCheck {
+ logger.info("READ_CAPABILITY_CONTAINER triggered. Our Response: READ_CAPABILITY_CONTAINER_RESPONSE")
+ readCapabilityContainerCheck = true
+ return READ_CAPABILITY_CONTAINER_RESPONSE
+ } // 00b000000f
+
+ /// Fourth command: NDEF Select command
+ if NDEF_SELECT_OK == commandApdu {
+ logger.info("NDEF_SELECT_OK triggered. Our Response: A_OKAY")
+ return A_OKAY
+ } // 00a4000c02e104
+
+ let uriCount = talerUri.count
+ let ndefLen = UInt16(uriCount + 5) // NDEF_RECORD_HEADER is 4 bytes
+ /// Fifth Command: Read the Length of the NDEF File
+ if NDEF_READ_BINARY_NLEN == commandApdu {
+ logger.info("NDEF_READ_BINARY_NLEN triggered. Our Response: length + A_OKAY")
+ readCapabilityContainerCheck = false
+
+ let ndefLenHi = UInt8(ndefLen >> 8)
+ let ndefLenLo = UInt8(ndefLen & 0xff)
+ let ndefLenLoData = withUnsafeBytes(of: ndefLenLo) { Data($0) }
+ var lenData = withUnsafeBytes(of: ndefLenHi) { Data($0) }
+ lenData.append(ndefLenLoData)
+ lenData.append(A_OKAY_ARRAY, count: 2)
+ return lenData
+ } // 00b0000002
+
+ /// Sixth Command: Read the NDEF File
+ if NDEF_READ_BINARY == commandApdu[0..<2] {
+ let count = commandApdu.count
+ if count > 4 {
+ var arrayApdu = Array<UInt8>(repeating: 0, count: count)
+ _ = arrayApdu.withUnsafeMutableBytes { commandApdu.copyBytes(to: $0) }
+// if arrayApdu[..<2] == NDEF_READ_BINARY[..<2] {
+// if count == 5 {
+ let length = arrayApdu[4]
+ let offset = UInt16(arrayApdu[3]) + (UInt16(arrayApdu[2]) << 8)
+ logger.info("NDEF_READ_BINARY triggered. Our Response: data + A_OKAY")
+// if offset == 2 && length == ndefLen {
+// var data = NDEF_RECORD_HEADER_SHORT
+ var data = NDEF_RECORD_HEADER_LONG
+ let uriCount8 = UInt8((uriCount + 1) & 0xff)
+ let lenData = withUnsafeBytes(of: uriCount8) { Data($0) }
+ data.append(lenData)
+ data.append("U".data(using: .utf8)!)
+ let zero: UInt8 = 0
+ let zeroData = withUnsafeBytes(of: zero) { Data($0) }
+ data.append(zeroData)
+ data.append(talerUri.data(using: .utf8)!)
+ data.append(A_OKAY)
+
+ readCapabilityContainerCheck = false
+ return data
+// }
+ }
+ }
+
+ // Reject GET_VERSION and MORE_INFO
+ if GET_VERSION == commandApdu {
+ logger.info("❗️GET_VERSION triggered. Our Response: GET_VERSION_RESPONSE❗️")
+ return GET_VERSION_RESPONSE
+ }
+ if MORE_INFO == commandApdu {
+ logger.info("❗️MORE_INFO triggered. Our Response: A_OKAY❗️")
+ return A_OKAY
+ }
+
+ //
+ // We're doing something outside our scope
+ //
+ logger.error("❗️processAPDU() | not yet implemented!")
+ return A_ERROR
+ } // processAPDU()
+
+}
diff --git a/Taler_Nightly.entitlements b/Taler_Nightly.entitlements
@@ -4,6 +4,12 @@
<dict>
<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>
+ <key>com.apple.developer.nfc.hce</key>
+ <true/>
+ <key>com.apple.developer.nfc.hce.iso7816.select-identifier-prefixes</key>
+ <array>
+ <string>D2760000850101</string>
+ </array>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>