Files
Emoney-Info---IOS/Emoney Info/Classes/api/BcaFlazzApi.swift

334 lines
11 KiB
Swift

//
// BcaFlazzApi.swift
// Emoney Info
//
// Created by Wira Irawan on 27/07/24.
//
import Foundation
import CoreNFC
public class BcaFlazzApi : UnifiedNfcApi {
private let emoney: Emoney = Emoney()
private var ef84Records: [RiwayatCard] = []
private var extendedRecords: [RiwayatCard] = []
private var historyRetryCount = 0
private let ef84MaxSlots = 10
private let extendedMaxRecords = 256
public override init() {}
public func checkFlazzCard() {
selectDirectoryFile(useFallbackLe: false)
}
private func selectDirectoryFile(useFallbackLe: Bool) {
let command = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xA4,
p1Parameter: 0x01,
p2Parameter: 0x00,
data: Data([0x02, 0x00]),
expectedResponseLength: useFallbackLe ? 256 : -1
)
apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
if response.sw1 == 0x90 && response.sw2 == 0x00 {
self.emoney.setCardLabel("BCA Flazz")
self.getCardNumber()
} else if !useFallbackLe {
self.selectDirectoryFile(useFallbackLe: true)
} else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
})
}
private func getCardNumber() {
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU02, completionHandler: { response in
if response.sw1 == 0x90 && response.sw2 == 0x00 {
guard let raw = String(data: response.getData(), encoding: .isoLatin1) else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
if let cardNumber = self.extractPan(from: raw) {
self.emoney.setCardNumber(cardNumber)
self.getBalance()
} else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
} else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
})
}
private func getBalance() {
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU03, completionHandler: { response in
if response.sw1 == 0x90 && response.sw2 == 0x00 {
let bytes = response.getData().bytes
guard bytes.count >= 4 else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
self.emoney.setBalance(self.uint32BE(bytes, offset: 0))
self.historyRetryCount = 0
self.resetHistoryState()
self.readHistoryCheck()
} else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
})
}
private func resetHistoryState() {
ef84Records.removeAll()
extendedRecords.removeAll()
}
private func retryHistoryRead(reason: String) {
if historyRetryCount < 1 {
historyRetryCount += 1
debugLog("Retrying Flazz history read: \(reason)")
resetHistoryState()
readHistoryCheck()
} else {
finishReading()
}
}
private func readHistoryCheck() {
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BCA_APDU04, completionHandler: { response in
let shouldReadExtended = response.sw1 == 0x90 && response.sw2 == 0x00
self.readEf84Record(slot: 0, shouldReadExtended: shouldReadExtended)
})
}
private func readEf84Record(slot: Int, shouldReadExtended: Bool) {
guard slot < ef84MaxSlots else {
if shouldReadExtended {
getChallenge()
} else {
finishReading()
}
return
}
let command = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xB0,
p1Parameter: 0x84,
p2Parameter: UInt8(slot * 0x0F),
data: Data(),
expectedResponseLength: 60
)
apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
if response.sw1 == 0x90 && response.sw2 == 0x00 {
if let record = self.parseEf84Record(response.getData()) {
self.ef84Records.append(record)
}
self.readEf84Record(slot: slot + 1, shouldReadExtended: shouldReadExtended)
} else if response.sw1 == 0x6B && response.sw2 == 0x00 {
if shouldReadExtended {
self.getChallenge()
} else {
self.finishReading()
}
} else {
self.retryHistoryRead(reason: "EF84 status \(response.sw1)-\(response.sw2) at slot \(slot)")
}
})
}
private func getChallenge() {
let command = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0x84,
p1Parameter: 0x00,
p2Parameter: 0x00,
data: Data(),
expectedResponseLength: 8
)
apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
if response.sw1 == 0x90 && response.sw2 == 0x00 {
self.readExtendedRecord(index: 0)
} else {
self.retryHistoryRead(reason: "challenge status \(response.sw1)-\(response.sw2)")
}
})
}
private func readExtendedRecord(index: Int) {
guard index < extendedMaxRecords else {
finishReading()
return
}
let command = NFCISO7816APDU(
instructionClass: 0x90,
instructionCode: 0x32,
p1Parameter: 0x03,
p2Parameter: 0x00,
data: Data([UInt8(index)]),
expectedResponseLength: 32
)
apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
if response.sw1 == 0x90 && response.sw2 == 0x00 {
if let record = self.parseExtendedRecord(response.getData()) {
self.extendedRecords.append(record)
}
self.readExtendedRecord(index: index + 1)
} else if response.sw1 == 0x6A && response.sw2 == 0x80 {
self.finishReading()
} else {
self.retryHistoryRead(reason: "extended status \(response.sw1)-\(response.sw2) at index \(index)")
}
})
}
private func finishReading() {
var history = ef84Records + extendedRecords
history.sort {
($0.getTransationTime() ?? Date.distantPast) > ($1.getTransationTime() ?? Date.distantPast)
}
if !history.isEmpty {
emoney.setRiwayatList(history)
emoney.setTampilRiwayat(true)
}
updateScreen()
apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
apduRunner.invalidateSession()
}
private func parseEf84Record(_ data: Data) -> RiwayatCard? {
let bytes = data.bytes
guard bytes.count >= 60 else {
return nil
}
let item = RiwayatCard()
let typeRaw = (Int(bytes[0]) << 8) | Int(bytes[1])
let amount = uint24BE(bytes, offset: 6)
let transactionTime = uint32BE(bytes, offset: 38)
let terminalId = asciiString(bytes, start: 30, length: 8)
let isPayment = typeRaw == 0x0400
item.setAmount(amount)
item.setLocationId(terminalId)
item.setLocationName(isPayment ? "payment".localizeString(string: self.langCode!) : "topup".localizeString(string: self.langCode!))
item.setTransactionTime(formatFlazzTimestamp(seconds: transactionTime))
if isPayment {
item.setProsesTipe(1)
item.setTitle("payment".localizeString(string: self.langCode!))
} else {
item.setProsesTipe(0)
item.setTitle("topup".localizeString(string: self.langCode!))
}
return transactionTime > 0 ? item : nil
}
private func parseExtendedRecord(_ data: Data) -> RiwayatCard? {
let bytes = data.bytes
guard bytes.count >= 32 else {
return nil
}
let item = RiwayatCard()
let type = bytes[0]
let rawTimestamp = Array(bytes[4..<8]).hexString().uppercased()
let timestampSeconds = uint32BE(bytes, offset: 4)
let rawAmount = uint24BE(bytes, offset: 1)
let terminalId = asciiString(bytes, start: 14, length: 8)
let isPayment = type == 0x04
let amount = isPayment ? 0x1000000 - rawAmount : rawAmount
item.setAmount(amount)
item.setLocationId(terminalId)
item.setLocationName(isPayment ? "payment".localizeString(string: self.langCode!) : "topup".localizeString(string: self.langCode!))
item.setDesk("RAW TS: \(rawTimestamp)")
item.setTransactionTime(formatFlazzTimestamp(seconds: timestampSeconds))
if isPayment {
item.setProsesTipe(1)
item.setTitle("payment".localizeString(string: self.langCode!))
} else {
item.setProsesTipe(0)
item.setTitle("topup".localizeString(string: self.langCode!))
}
return timestampSeconds > 0 ? item : nil
}
private func extractPan(from raw: String) -> String? {
guard let start = raw.firstIndex(of: ";") else {
return nil
}
let tail = raw[raw.index(after: start)...]
guard let end = tail.firstIndex(of: "=") else {
return nil
}
return String(tail[..<end])
}
private func uint24BE(_ bytes: [UInt8], offset: Int) -> Int {
guard bytes.count >= offset + 3 else {
return 0
}
return (Int(bytes[offset]) << 16)
| (Int(bytes[offset + 1]) << 8)
| Int(bytes[offset + 2])
}
private func uint32BE(_ bytes: [UInt8], offset: Int) -> Int {
guard bytes.count >= offset + 4 else {
return 0
}
return (Int(bytes[offset]) << 24)
| (Int(bytes[offset + 1]) << 16)
| (Int(bytes[offset + 2]) << 8)
| Int(bytes[offset + 3])
}
private func formatFlazzTimestamp(seconds: Int) -> Date {
var dateComponents = DateComponents()
dateComponents.year = 1980
dateComponents.month = 1
dateComponents.day = 1
dateComponents.timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
dateComponents.hour = 0
dateComponents.minute = 0
dateComponents.second = seconds
return Calendar.current.date(from: dateComponents) ?? Date.distantPast
}
private func asciiString(_ bytes: [UInt8], start: Int, length: Int) -> String {
guard bytes.count >= start + length else {
return ""
}
let slice = Array(bytes[start..<(start + length)])
return String(bytes: slice, encoding: .ascii)?
.trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters)) ?? ""
}
private func updateScreen() {
if self.apduRunner.callback != nil {
self.apduRunner.callback?.complete(emoney: self.emoney)
}
}
}