Adds BML contactless token fetching, APDU emulation utility, TapToPayViewModel, and TapToPayView with NFC animation. Also updates QR scanner, transfer flow, dashboard, and home views. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
5.5 KiB
Swift
172 lines
5.5 KiB
Swift
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..<next], radix: 16) ?? 0)
|
|
index = next
|
|
}
|
|
return result
|
|
}
|
|
|
|
static func hexToData(_ hex: String) -> Data {
|
|
Data(hexToBytes(hex))
|
|
}
|
|
|
|
static func asciiToHex(_ s: String) -> String {
|
|
s.compactMap { $0.asciiValue }.map { String(format: "%02X", $0) }.joined()
|
|
}
|
|
}
|