import Foundation enum BmlContactlessUtil { // MARK: - APDU Instruction Bytes static let INS_SELECT: UInt8 = 0xA4 static let INS_GPO: UInt8 = 0xA8 static let INS_READ: UInt8 = 0xB2 // MARK: - PPSE static let PPSE_HEX = "325041592E5359532E4444463031" static let PPSE_BYTES: [UInt8] = [ 0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31 ] // MARK: - Status Words static let SW_OK_HEX = "9000" static let SW_OK = Data([0x90, 0x00]) static let SW_UNKNOWN_ERROR = Data([0x6F, 0x00]) static let SW_INS_NOT_SUPPORTED = Data([0x6D, 0x00]) // MARK: - Active token static var activeToken: BmlWalletToken? static var onTransactionComplete: ((Bool) -> Void)? static func setToken(_ token: BmlWalletToken) { activeToken = token } static func clearToken() { activeToken = nil; onTransactionComplete = nil } // MARK: - Application label static func applicationLabel(aidHex: String) -> String { if aidHex.hasPrefix("A0000000031010") { return "VISA" } if aidHex.hasPrefix("A0000000041010") { return "MASTERCARD" } if aidHex.hasPrefix("A000000025") { return "AMEX" } return "BML" } // MARK: - APDU Processing /// Process an APDU command and return the response bytes. /// Returns nil if the APDU is malformed. static func processCommandApdu(_ command: Data) -> Data { guard command.count >= 5 else { return SW_UNKNOWN_ERROR } let ins = command[1] switch ins { case INS_SELECT: return handleSelect(command) case INS_GPO: return handleGpo() case INS_READ: return handleReadRecord() default: return SW_INS_NOT_SUPPORTED } } // MARK: - APDU Handlers private static func handleSelect(_ command: Data) -> Data { let lc = Int(command[4]) guard command.count >= 5 + lc, lc > 0 else { return SW_UNKNOWN_ERROR } let data = command.subdata(in: 5..<5 + lc) if data == Data(PPSE_BYTES) { guard let token = activeToken else { return SW_UNKNOWN_ERROR } return hexToData(buildSelectPpseResponse( aid: token.appCode, label: applicationLabel(aidHex: token.appCode), priority: "01" )) } guard let token = activeToken else { return SW_UNKNOWN_ERROR } let aidBytes = Data(hexToBytes(token.appCode)) if data == aidBytes { return hexToData(buildSelectAidResponse( aid: token.appCode, label: applicationLabel(aidHex: token.appCode) )) } return SW_UNKNOWN_ERROR } private static func handleGpo() -> Data { let miscData = "008008010100" let body = tlv("80", miscData) return hexToData(body + SW_OK_HEX) } private static func handleReadRecord() -> Data { guard let token = activeToken else { return SW_UNKNOWN_ERROR } let track2 = buildTrack2(token) let body = tlv("70", tlv("57", track2)) let response = hexToData(body + SW_OK_HEX) onTransactionComplete?(true) return response } // MARK: - Response Builders static func buildSelectPpseResponse(aid: String, label: String, priority: String) -> String { let priorityTlv = tlv("87", priority) let aidTlv = tlv("4F", aid) let appEntry = tlv("61", aidTlv + priorityTlv) let ppseTlv = tlv("84", PPSE_HEX) let inner = tlv("BF0C", appEntry) let propTpl = tlv("A5", inner) let fci = tlv("6F", ppseTlv + propTpl) return fci + SW_OK_HEX } static func buildSelectAidResponse(aid: String, label: String) -> String { let aidTlv = tlv("84", aid) let labelTlv = tlv("50", asciiToHex(label)) let pdolTlv = tlv("9F38", "9F6602") let propTpl = tlv("A5", labelTlv + pdolTlv) let fci = tlv("6F", aidTlv + propTpl) return fci + SW_OK_HEX } static func buildTrack2(_ token: BmlWalletToken) -> String { var t2 = "\(token.token)D\(token.expiry)\(token.serviceCode)\(token.data)" if t2.count % 2 != 0 { t2 += "F" } return t2 } // MARK: - TLV Encoding /// Builds a BER-TLV triplet: tag (hex, 1-2 bytes) + DER length + data (hex). static func tlv(_ tagHex: String, _ dataHex: String) -> String { let lenBytes = dataHex.count / 2 let lenHex: String switch lenBytes { case 0...0x7F: lenHex = String(format: "%02X", lenBytes) case 0x80...0xFF: lenHex = "81" + String(format: "%02X", lenBytes) default: lenHex = "82" + String(format: "%02X", lenBytes >> 8) + String(format: "%02X", lenBytes & 0xFF) } return tagHex + lenHex + dataHex } // MARK: - Hex Utilities static func hexToBytes(_ hex: String) -> [UInt8] { let s = hex.uppercased() var result: [UInt8] = [] var index = s.startIndex while index < s.endIndex { let next = s.index(index, offsetBy: 2) result.append(UInt8(s[index.. Data { Data(hexToBytes(hex)) } static func asciiToHex(_ s: String) -> String { s.compactMap { $0.asciiValue }.map { String(format: "%02X", $0) }.joined() } }