Improve NFC history readers and prepare production build

This commit is contained in:
2026-05-08 05:40:52 +07:00
parent 1dc293c697
commit bd34467ddc
14 changed files with 688 additions and 182 deletions

View File

@ -12,6 +12,7 @@ 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
@ -74,6 +75,8 @@ public class BcaFlazzApi : UnifiedNfcApi {
}
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!))
@ -81,6 +84,22 @@ public class BcaFlazzApi : UnifiedNfcApi {
})
}
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
@ -120,11 +139,7 @@ public class BcaFlazzApi : UnifiedNfcApi {
self.finishReading()
}
} else {
if shouldReadExtended {
self.getChallenge()
} else {
self.finishReading()
}
self.retryHistoryRead(reason: "EF84 status \(response.sw1)-\(response.sw2) at slot \(slot)")
}
})
}
@ -143,7 +158,7 @@ public class BcaFlazzApi : UnifiedNfcApi {
if response.sw1 == 0x90 && response.sw2 == 0x00 {
self.readExtendedRecord(index: 0)
} else {
self.finishReading()
self.retryHistoryRead(reason: "challenge status \(response.sw1)-\(response.sw2)")
}
})
}
@ -172,7 +187,7 @@ public class BcaFlazzApi : UnifiedNfcApi {
} else if response.sw1 == 0x6A && response.sw2 == 0x80 {
self.finishReading()
} else {
self.finishReading()
self.retryHistoryRead(reason: "extended status \(response.sw1)-\(response.sw2) at index \(index)")
}
})
}
@ -293,11 +308,11 @@ public class BcaFlazzApi : UnifiedNfcApi {
dateComponents.year = 1980
dateComponents.month = 1
dateComponents.day = 1
dateComponents.timeZone = TimeZone(identifier: "Asia/Jakarta")!
dateComponents.timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
dateComponents.hour = 0
dateComponents.minute = 0
dateComponents.second = seconds
return Calendar.current.date(from: dateComponents)!
return Calendar.current.date(from: dateComponents) ?? Date.distantPast
}
private func asciiString(_ bytes: [UInt8], start: Int, length: Int) -> String {

View File

@ -13,6 +13,7 @@ public class BrizziApi : UnifiedNfcApi {
var riwayatList: [RiwayatCard] = []
var uid : String?
var rawLog : String = ""
private var historyRetryCount = 0
public override init() {}
@ -73,24 +74,44 @@ public class BrizziApi : UnifiedNfcApi {
let brizziSamHelper = BrizziSamHelper()
brizziSamHelper.keyCard = data.hexEncodedString()
let random = "8DC0DC40FE1DC582CF7099E2AACFBC10".hex2byte()
let command = self.emoney.getCardNumber() + self.uid! + "FF"
let decrypted = BrizziSamHelper.decryptDeSeDe(random)?.hexEncodedString()
guard let uid = self.uid else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
let command = self.emoney.getCardNumber() + uid + "FF"
guard let decrypted = BrizziSamHelper.decryptDeSeDe(random)?.hexEncodedString() else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
let decryptedFinal = decrypted?.subString(from: 0, to: 32)
let encrypted = BrizziSamHelper.encryptDeSeDe(command, decryptedFinal!, "0000000000000000")?.hexEncodedString()
let decryptedFinal = decrypted.subString(from: 0, to: 32)
guard decryptedFinal.count == 32,
let encrypted = BrizziSamHelper.encryptDeSeDe(command, decryptedFinal, "0000000000000000")?.hexEncodedString() else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
brizziSamHelper.encryptedKey = encrypted?.subString(from: 0, to: 32)
brizziSamHelper.encryptedKey = encrypted.subString(from: 0, to: 32)
let randomHex = "3C37029CA595FE4E7E62FCB2F7909B2C".hex2byte()
let randomHexDecrypted = BrizziSamHelper.decryptDeSeDe(randomHex)
let randomHexFinal = randomHexDecrypted?.hexEncodedString().subString(from: 0, to: 32) ?? ""
let randomHexFinal = randomHexDecrypted?.hexEncodedString().subString(from: 0, to: 32)
let randomHexEncrypted = BrizziSamHelper.encryptDeSeDe(brizziSamHelper.encryptedKey!, randomHexFinal!, brizziSamHelper.authKey)
brizziSamHelper.random = (randomHexEncrypted?.hexEncodedString())!.subString(from: 0, to: 32)
guard randomHexFinal.count == 32,
let encryptedKey = brizziSamHelper.encryptedKey,
let randomHexEncrypted = BrizziSamHelper.encryptDeSeDe(encryptedKey, randomHexFinal, brizziSamHelper.authKey)?.hexEncodedString() else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
brizziSamHelper.random = randomHexEncrypted.subString(from: 0, to: 32)
let samChallenge = brizziSamHelper.generateSamRandom().subString(from: 0, to: 32)
guard samChallenge.count == 32 else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
let BRI_APDU04 = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0xAF, p1Parameter : 0x00, p2Parameter : 0x00, data : Data(_: samChallenge.hex2byte()), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
apduRunner.exchangeApdu(apduCommand: BRI_APDU04, completionHandler: {response in
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x91 && response.sw2 == 0xAF){
@ -105,12 +126,42 @@ public class BrizziApi : UnifiedNfcApi {
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_APDU05, completionHandler: {response in
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x91 && response.sw2 == 0xAF){
self.emoney.setBalance(self.getRealBalance(reverseHexa: response.getData().hexEncodedString().subString(from: 0, to: 8)))
self.getLog()
self.historyRetryCount = 0
self.startHistoryRead()
} else {
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
})
}
private func startHistoryRead() {
rawLog = ""
riwayatList.removeAll()
getLog()
}
private func retryHistoryRead(reason: String) {
if historyRetryCount < 1 {
historyRetryCount += 1
debugLog("Retrying Brizzi history read: \(reason)")
startHistoryRead()
} else {
finalizeHistoryRead()
}
}
private func finalizeHistoryRead() {
if (self.parseLog()){
self.riwayatList = self.riwayatList.sorted {
($0.getTransationTime() ?? Date.distantPast) > ($1.getTransationTime() ?? Date.distantPast)
}
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
}
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
}
private func getLog(){
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.BRI_LOG01, completionHandler: {response in
@ -118,9 +169,7 @@ public class BrizziApi : UnifiedNfcApi {
self.rawLog.append(response.getData().hexEncodedString())
self.getMoreLog()
} else {
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
self.retryHistoryRead(reason: "initial log status \(response.sw1)-\(response.sw2)")
}
})
}
@ -131,16 +180,12 @@ public class BrizziApi : UnifiedNfcApi {
self.rawLog.append(response.getData().hexEncodedString())
self.getMoreLog()
} else {
self.rawLog.append(response.getData().hexEncodedString())
if (self.parseLog()){
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
if (response.sw1 == 0x91 && response.sw2 == 0x00) || (response.sw1 == 0x90 && response.sw2 == 0x00) {
self.rawLog.append(response.getData().hexEncodedString())
self.finalizeHistoryRead()
} else {
self.retryHistoryRead(reason: "continuation log status \(response.sw1)-\(response.sw2)")
}
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
}
})
}
@ -163,7 +208,11 @@ public class BrizziApi : UnifiedNfcApi {
riwayat.setAmount(self.getRealBalance(reverseHexa: data.subString(from: 46, to: 52)))
let time = self.getTransactionTime(formatDate: data.subString(from: 32, to: 38), formatTime: data.subString(from: 38, to: 44))
riwayat.setTransactionTime(time!)
guard let time else {
debugLog("Skipping Brizzi log with invalid timestamp: \(data)")
continue
}
riwayat.setTransactionTime(time)
riwayatList.append(riwayat)
}
@ -173,8 +222,11 @@ public class BrizziApi : UnifiedNfcApi {
func getTransactionTime(formatDate : String, formatTime : String) -> Date?{
let dateFormatter2 = DateFormatter()
dateFormatter2.locale = Locale(identifier: "en_US_POSIX")
dateFormatter2.dateFormat = "HHmmss"
let date12 = dateFormatter2.date(from: formatTime)!
guard let date12 = dateFormatter2.date(from: formatTime) else {
return nil
}
dateFormatter2.dateFormat = "hh:mm a"
let date22 = dateFormatter2.string(from: date12)

View File

@ -16,88 +16,375 @@ public class MandiriEmoneyApi : UnifiedNfcApi {
var start = 0
var finish = 256
var finish2 = 10
private var historyRetryCount = 0
private let useMandiriSpecHistoryReader = true
private let mandiriSpecMaxPage = 4
private let mandiriSpecEmptyPageStopThreshold = 2
public override init() {}
private func logMandiri(_ message: String) {
_ = message
// print("[Mandiri] \(message)")
// debugLog("[Mandiri] \(message)")
}
private func logMandiriApdu(_ label: String, command: NFCISO7816APDU) {
logMandiri("APDU -> \(label) | \(command.toHexString())")
}
private func logMandiriResponse(_ label: String, response: ApduResponse) {
logMandiri(
String(
format: "APDU <- %@ | sw1=%02X sw2=%02X data=%@",
label,
response.sw1 ?? 0,
response.sw2 ?? 0,
response.getData().hexEncodedString()
)
)
}
public func getCardNumber(){
logMandiriApdu("MANDIRI_APDU01", command: EmoneyApduCommands.MANDIRI_APDU01)
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.MANDIRI_APDU01, completionHandler: {response in
self.logMandiriResponse("MANDIRI_APDU01", response: response)
if (response.sw1 == 0x90 && response.sw2 == 0x00){
let raw = response.getData().hexEncodedString()
self.emoney.setCardLabel("Mandiri e-Money")
self.emoney.setCardNumber(response.getData().hexEncodedString().subString(from: 0, to: 16))
self.cardType = response.getData().hexEncodedString().subString(from: 36, to: 38).hex2decimal()
debugLog(self.cardType!)
self.emoney.setCardNumber(raw.subString(from: 0, to: 16))
self.cardType = raw.subString(from: 36, to: 38).hex2decimal()
self.logMandiri("Card info raw=\(raw)")
self.logMandiri("Card parsed | number=\(self.emoney.getCardNumber()) | cardType=\(self.cardType ?? -1)")
self.getBalance()
} else {
self.logMandiri("Card info failed | status=\(response.sw1)-\(response.sw2)")
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
})
}
private func getBalance(){
logMandiriApdu("MANDIRI_APDU02", command: EmoneyApduCommands.MANDIRI_APDU02)
apduRunner.exchangeApdu(apduCommand: EmoneyApduCommands.MANDIRI_APDU02, completionHandler: {response in
self.logMandiriResponse("MANDIRI_APDU02", response: response)
if (response.sw1 == 0x90 && response.sw2 == 0x00){
debugLog(response.getData().hexEncodedString())
let balance = response.getData().hexEncodedString().subString(from: 0, to: 8)
let raw = response.getData().hexEncodedString()
let balance = raw.subString(from: 0, to: 8)
self.emoney.setBalance(self.getRealBalance(reverseHexa: balance))
self.logMandiri("Balance raw=\(raw)")
self.logMandiri("Balance parsed=\(self.emoney.getBalance())")
self.resetHistoryState()
// self.updateScreen()
if (self.cardType! == 131){
self.getLogStep01(index: self.start)
} else {
self.getLogStep02(index: self.start)
}
self.historyRetryCount = 0
self.startHistoryRead()
} else {
self.logMandiri("Balance read failed | status=\(response.sw1)-\(response.sw2)")
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
}
})
}
private func startHistoryRead() {
resetHistoryState()
logMandiri("Start history read | retry=\(historyRetryCount) | cardType=\(cardType ?? -1) | useSpec=\(useMandiriSpecHistoryReader)")
if useMandiriSpecHistoryReader {
startMandiriSpecHistoryRead()
return
}
if (self.cardType == 131){
self.getLogStep01(index: self.start)
} else {
self.getLogStep02(index: self.start)
}
}
private func retryHistoryRead(reason: String, retryable: Bool = true) {
guard retryable else {
logMandiri("History read not retryable | reason=\(reason)")
finalizeHistoryResult()
return
}
if historyRetryCount < 1 {
historyRetryCount += 1
logMandiri("Retrying history read | reason=\(reason) | retry=\(historyRetryCount)")
startHistoryRead()
} else {
logMandiri("Retry exhausted | reason=\(reason)")
finalizeHistoryResult()
}
}
private func finalizeHistoryResult() {
self.start = 0
self.finalizeHistory()
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(!self.riwayatList.isEmpty)
self.logMandiri("Finalize history | count=\(self.riwayatList.count)")
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
}
// MARK: - New Mandiri Spec Reader
// Toggle `useMandiriSpecHistoryReader` to rollback to the legacy implementation.
private func startMandiriSpecHistoryRead() {
logMandiri("Start spec history read | pages=1...\(mandiriSpecMaxPage)")
readMandiriSpecPage(page: 1, collectedChunks: [], emptyPageStreak: 0)
}
private func readMandiriSpecPage(page: Int, collectedChunks: [String], emptyPageStreak: Int) {
guard page <= mandiriSpecMaxPage else {
probeMandiriSpecStop(collectedChunks: collectedChunks)
return
}
let command = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xD1,
p1Parameter: UInt8(page & 0xFF),
p2Parameter: 0x00,
data: Data(),
expectedResponseLength: CommonConstants.LE_GET_ALL_RESPONSE_DATA
)
logMandiriApdu("MANDIRI_SPEC_PAGE[\(page)]", command: command)
apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
self.logMandiriResponse("MANDIRI_SPEC_PAGE[\(page)]", response: response)
if response.sw1 == 0x90 && response.sw2 == 0x00 {
let chunk = response.getData().hexEncodedString()
let isEmptyPage = self.isMandiriSpecPageEmpty(chunk)
self.logMandiri("Spec page chunk | page=\(page) | rawLength=\(chunk.count) | empty=\(isEmptyPage)")
var nextChunks = collectedChunks
nextChunks.append(chunk)
let nextEmptyPageStreak = isEmptyPage ? (emptyPageStreak + 1) : 0
if nextEmptyPageStreak >= self.mandiriSpecEmptyPageStopThreshold {
self.logMandiri("Spec page empty streak reached threshold | streak=\(nextEmptyPageStreak) | stopAfterPage=\(page)")
self.probeMandiriSpecStop(collectedChunks: nextChunks)
return
}
self.readMandiriSpecPage(page: page + 1, collectedChunks: nextChunks, emptyPageStreak: nextEmptyPageStreak)
} else {
self.logMandiri("Spec page read failed | page=\(page) | status=\(response.sw1)-\(response.sw2)")
self.retryHistoryRead(reason: "spec page status \(response.sw1)-\(response.sw2) at page \(page)")
}
})
}
private func probeMandiriSpecStop(collectedChunks: [String]) {
let probePage = mandiriSpecMaxPage + 1
let command = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xD1,
p1Parameter: UInt8(probePage & 0xFF),
p2Parameter: 0x00,
data: Data(),
expectedResponseLength: CommonConstants.LE_GET_ALL_RESPONSE_DATA
)
logMandiriApdu("MANDIRI_SPEC_PROBE[\(probePage)]", command: command)
apduRunner.exchangeApdu(apduCommand: command, completionHandler: { response in
self.logMandiriResponse("MANDIRI_SPEC_PROBE[\(probePage)]", response: response)
if response.sw1 == 0x69 && response.sw2 == 0x86 {
self.logMandiri("Spec probe stop confirmed | page=\(probePage)")
} else {
self.logMandiri("Spec probe unexpected status | page=\(probePage) | status=\(response.sw1)-\(response.sw2)")
}
self.mapv1 = collectedChunks.joined()
if self.parseMandiriSpecLog() {
self.finalizeHistoryResult()
} else {
self.retryHistoryRead(reason: "invalid spec history payload", retryable: false)
}
})
}
private func parseMandiriSpecLog() -> Bool {
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
let recordHexLength = 48
logMandiri("Parse spec log | rawLength=\(logs.count)")
guard logs.count >= recordHexLength, logs.count % recordHexLength == 0 else {
logMandiri("Parse spec log invalid length | actual=\(logs.count) | recordHexLength=\(recordHexLength)")
return false
}
let total = logs.count / recordHexLength
logMandiri("Parse spec log totalRecords=\(total)")
for i in 0..<total {
let start = i * recordHexLength
let end = start + recordHexLength
let recordHex = logs.subString(from: start, to: end)
guard let riwayat = parseMandiriSpecRecord(recordHex, index: i) else {
logMandiri("Skip spec log record | index=\(i) | raw=\(recordHex)")
continue
}
riwayatList.append(riwayat)
}
return true
}
private func parseMandiriSpecRecord(_ recordHex: String, index: Int) -> RiwayatCard? {
guard recordHex.count == 48 else {
logMandiri("Spec record invalid length | index=\(index) | length=\(recordHex.count)")
return nil
}
if isMandiriSpecRecordEmpty(recordHex) {
logMandiri("Spec record empty | index=\(index)")
return nil
}
let bytes = recordHex.hex2byte().bytes
guard bytes.count == 24 else {
logMandiri("Spec record invalid byte count | index=\(index) | bytes=\(bytes.count)")
return nil
}
guard let time = decodeMandiriSpecTimestamp(bytes, index: index) else {
logMandiri("Spec record invalid timestamp | index=\(index) | raw=\(recordHex)")
return nil
}
let transactionType = bytes[15]
let amount = decodeMandiriLittleEndianUInt32(Array(bytes[16...19]))
let balanceAfter = decodeMandiriLittleEndianUInt32(Array(bytes[20...23]))
let terminalData = Array(bytes[7...13]).map { String(format: "%02X", $0) }.joined()
let riwayat = RiwayatCard()
riwayat.setTransactionTime(time)
riwayat.setAmount(amount)
riwayat.setDesk("BAL_AFTER=\(balanceAfter)")
riwayat.setLocationId(terminalData)
if transactionType == 0x00 {
riwayat.setProsesTipe(0)
riwayat.setTitle("topup".localizeString(string: self.langCode!))
} else if transactionType == 0x20 {
riwayat.setProsesTipe(1)
riwayat.setTitle("payment".localizeString(string: self.langCode!))
} else {
riwayat.setProsesTipe(-1)
riwayat.setTitle("unknown".localizeString(string: self.langCode!))
}
logMandiri(
String(
format: "Parsed spec log | index=%d | type=%02X | title=%@ | amount=%d | balanceAfter=%d | terminal=%@ | raw=%@",
index,
transactionType,
riwayat.getTitle() ?? "-",
amount,
balanceAfter,
terminalData,
recordHex
)
)
return riwayat
}
private func isMandiriSpecPageEmpty(_ pageHex: String) -> Bool {
guard !pageHex.isEmpty else {
return true
}
return !pageHex.contains { $0 != "0" }
}
private func isMandiriSpecRecordEmpty(_ recordHex: String) -> Bool {
return !recordHex.contains { $0 != "0" }
}
private func decodeMandiriSpecTimestamp(_ bytes: [UInt8], index: Int) -> Date? {
guard bytes.count >= 7 else {
return nil
}
let values = bytes.prefix(7).map { decodeBcd($0) }
guard values.allSatisfy({ $0 >= 0 }) else {
logMandiri("BCD decode failed | index=\(index) | bytes=\(bytes.prefix(7).map { String(format: "%02X", $0) }.joined())")
return nil
}
var components = DateComponents()
components.calendar = Calendar(identifier: .gregorian)
components.timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
components.year = 2000 + values[2]
components.month = values[1]
components.day = values[0]
components.hour = values[3]
components.minute = values[4]
components.second = values[5]
components.nanosecond = values[6] * 10_000_000
return components.date
}
private func decodeBcd(_ value: UInt8) -> Int {
let high = Int((value & 0xF0) >> 4)
let low = Int(value & 0x0F)
guard high < 10, low < 10 else {
return -1
}
return (high * 10) + low
}
private func decodeMandiriLittleEndianUInt32(_ bytes: [UInt8]) -> Int {
guard bytes.count >= 4 else {
return 0
}
return Int(bytes[0])
| (Int(bytes[1]) << 8)
| (Int(bytes[2]) << 16)
| (Int(bytes[3]) << 24)
}
private func getLogStep01(index : Int){
//00 d1 00 00 00
let hex = String(index, radix: 16)
let MANDIRI_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xD1, p1Parameter : hex.hex2byte().bytes.first!, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
let p1Parameter = hex.hex2byte().bytes.first ?? 0x00
let MANDIRI_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xD1, p1Parameter : p1Parameter, p2Parameter : 0x00, data : Data(), expectedResponseLength : CommonConstants.LE_GET_ALL_RESPONSE_DATA)
logMandiriApdu("MANDIRI_LOG_NEW[\(index)]", command: MANDIRI_LOG)
apduRunner.exchangeApdu(apduCommand: MANDIRI_LOG, completionHandler: {response in
self.logMandiriResponse("MANDIRI_LOG_NEW[\(index)]", response: response)
if (response.sw1 == 0x90 && response.sw2 == 0x00){
self.mapv1.append(response.getData().hexEncodedString())
let chunk = response.getData().hexEncodedString()
self.mapv1.append(chunk)
self.logMandiri("New log chunk | index=\(index) | rawLength=\(chunk.count) | accumulated=\(self.mapv1.count)")
self.start+=1
if (self.start < (self.finish)){
self.getLogStep01(index: self.start)
} else {
self.parseNewLog()
self.start = 0
self.finalizeHistory()
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
if self.parseNewLog() {
self.finalizeHistoryResult()
} else {
self.retryHistoryRead(reason: "invalid new history payload", retryable: false)
}
}
} else {
self.parseNewLog()
self.start = 0
self.finalizeHistory()
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
self.logMandiri("New log read failed | index=\(index) | status=\(response.sw1)-\(response.sw2)")
self.retryHistoryRead(reason: "new history status \(response.sw1)-\(response.sw2) at index \(index)")
}
})
}
func parseNewLog(){
func parseNewLog() -> Bool {
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
logMandiri("Parse new log | rawLength=\(logs.count)")
if (logs.count % 48 != 0){
return
logMandiri("Parse new log invalid length | length=\(logs.count)")
return false
}
let total = logs.count/48
logMandiri("Parse new log totalRecords=\(total)")
for i in 0..<total {
let start = i*48
let end = start + 48
let data = logs.subString(from: start, to: end)
let riwayat = RiwayatCard()
let time = self.getTransactionTime(formatDate: data.subString(from: 0, to: 6), formatTime: data.subString(from: 6, to: 12))
riwayat.setTransactionTime(time!)
guard let time else {
self.logMandiri("Skip new log record | index=\(i) | reason=invalid_timestamp | raw=\(data)")
continue
}
riwayat.setTransactionTime(time)
let processType = Int(data.subString(from: 28, to: 32))
if (processType == 100){
riwayat.setProsesTipe(0)
@ -109,69 +396,78 @@ public class MandiriEmoneyApi : UnifiedNfcApi {
riwayat.setLocationId(data.subString(from: 12, to: 20))
let amount = data.subString(from: 32, to: 40)
riwayat.setAmount(getRealBalance(reverseHexa: amount))
self.logMandiri("Parsed new log | index=\(i) | title=\(riwayat.getTitle() ?? "-") | processType=\(processType ?? -1) | locationId=\(riwayat.getLocationId() ?? "-") | amount=\(riwayat.getAmount()) | raw=\(data)")
riwayatList.append(riwayat)
}
return true
}
private func getLogStep02(index : Int){
//00 b2 00 00 1e
let hex = String(index, radix: 16)
let MANDIRI_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB2, p1Parameter : hex.hex2byte().bytes.first!, p2Parameter : 0x00, data : Data(), expectedResponseLength : 30)
let p1Parameter = hex.hex2byte().bytes.first ?? 0x00
let MANDIRI_LOG = NFCISO7816APDU(instructionClass : 0x00, instructionCode : 0xB2, p1Parameter : p1Parameter, p2Parameter : 0x00, data : Data(), expectedResponseLength : 30)
logMandiriApdu("MANDIRI_LOG_OLD[\(index)]", command: MANDIRI_LOG)
apduRunner.exchangeApdu(apduCommand: MANDIRI_LOG, completionHandler: {response in
self.logMandiriResponse("MANDIRI_LOG_OLD[\(index)]", response: response)
if (response.sw1 == 0x90 && response.sw2 == 0x00){
self.mapv1.append(response.getData().hexEncodedString())
let chunk = response.getData().hexEncodedString()
self.mapv1.append(chunk)
self.logMandiri("Old log chunk | index=\(index) | rawLength=\(chunk.count) | accumulated=\(self.mapv1.count)")
self.start+=1
if (self.start < (self.finish2)){
self.getLogStep02(index: self.start)
} else {
self.parseOldLog()
self.start = 0
self.finalizeHistory()
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
if self.parseOldLog() {
self.finalizeHistoryResult()
} else {
self.retryHistoryRead(reason: "invalid old history payload", retryable: false)
}
}
} else {
self.parseOldLog()
self.start = 0
self.finalizeHistory()
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
self.updateScreen()
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
self.logMandiri("Old log read failed | index=\(index) | status=\(response.sw1)-\(response.sw2)")
self.retryHistoryRead(reason: "old history status \(response.sw1)-\(response.sw2) at index \(index)")
}
})
}
func parseOldLog(){
func parseOldLog() -> Bool {
let logs = self.mapv1.trimmingCharacters(in: .whitespacesAndNewlines)
logMandiri("Parse old log | rawLength=\(logs.count)")
if (logs.count % 60 != 0){
return
logMandiri("Parse old log invalid length | length=\(logs.count)")
return false
}
let total = logs.count/60
logMandiri("Parse old log totalRecords=\(total)")
for i in 0..<total {
let start = i*60
let end = start + 60
let data = logs.subString(from: start, to: end)
let riwayat = riwayatCard(data.hex2byte().bytes)
riwayatList.append(riwayat!)
if let riwayat = riwayatCard(data.hex2byte().bytes) {
self.logMandiri("Parsed old log | index=\(i) | title=\(riwayat.getTitle() ?? "-") | amount=\(riwayat.getAmount()) | raw=\(data)")
riwayatList.append(riwayat)
} else {
self.logMandiri("Skip old log record | index=\(i) | raw=\(data)")
}
}
return true
}
func getTransactionTime(formatDate : String, formatTime : String) -> Date?{
let dateFormatter2 = DateFormatter()
dateFormatter2.locale = Locale(identifier: "en_US_POSIX")
dateFormatter2.dateFormat = "HHmmss"
let date12 = dateFormatter2.date(from: formatTime)!
guard let date12 = dateFormatter2.date(from: formatTime) else {
return nil
}
dateFormatter2.dateFormat = "hh:mm a"
let date22 = dateFormatter2.string(from: date12)
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "ddMMyy hh:mm a"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "ddMMyy hh:mm a"
return dateFormatter.date(from:(formatDate + " " + date22))
}
@ -207,7 +503,18 @@ public class MandiriEmoneyApi : UnifiedNfcApi {
}
private func finalizeHistory() {
riwayatList = riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
riwayatList = riwayatList.sorted { lhs, rhs in
switch (lhs.getTransationTime(), rhs.getTransationTime()) {
case let (left?, right?):
return left.compare(right) == .orderedDescending
case (_?, nil):
return true
case (nil, _?):
return false
case (nil, nil):
return false
}
}
}
func riwayatCard(_ bArr: [UInt8]) -> RiwayatCard? {
@ -229,9 +536,9 @@ public class MandiriEmoneyApi : UnifiedNfcApi {
do {
wrap.copyBytes(to: &bArr3, count: 16)
debugLog("bArr3: \(bArr3.map { String(format: "%02X", $0) }.joined())")
logMandiri("Old log detail bytes=\(bArr3.map { String(format: "%02X", $0) }.joined())")
} catch {
debugLog("Error: \(error.localizedDescription)")
logMandiri("Old log detail error=\(error.localizedDescription)")
}
var type = 0
@ -252,8 +559,11 @@ public class MandiriEmoneyApi : UnifiedNfcApi {
str2 += String(format: "%02X", bArr2[y])
}
let transactionTime = self.getTransactionTime(formatDate: str.subString(from: 0, to: 6), formatTime: str.subString(from: 6, to: 12))
riwayatCard.setTransactionTime(transactionTime!)
guard let transactionTime = self.getTransactionTime(formatDate: str.subString(from: 0, to: 6), formatTime: str.subString(from: 6, to: 12)) else {
logMandiri("Skip old log record | reason=invalid_timestamp | raw=\(str)")
return nil
}
riwayatCard.setTransactionTime(transactionTime)
riwayatCard.setAmount(Int(amount))
// if str.caseInsensitiveCompare("payment") == .orderedSame {
@ -266,6 +576,7 @@ public class MandiriEmoneyApi : UnifiedNfcApi {
riwayatCard.setTitle(str)
if type == -1 {
logMandiri("Skip old log record | reason=unknown_type | len=\(len) | raw=\(str)")
return nil
}

View File

@ -14,6 +14,7 @@ public class TapCashApi : UnifiedNfcApi {
var riwayatList: [RiwayatCard] = []
var start = 0
var totalLog = 0
private var historyRetryCount = 0
public override init() {}
@ -22,15 +23,22 @@ public class TapCashApi : UnifiedNfcApi {
if (response.sw1 == 0x90 && response.sw2 == 0x00){
self.emoney.setCardLabel("BNI TapCash")
self.tapCashData.setPurseData(response.getData().bytes)
let balance = self.tapCashData.getPurseBalance()?.hexString().hex2decimal()
self.emoney.setBalance(balance!)
self.emoney.setCardNumber(self.tapCashData.getCAN()!.hexString())
self.totalLog = (self.tapCashData.getTotalRecords()?.hexString().hex2decimal())!
let balance = self.tapCashData.getPurseBalance()?.hexString().hex2decimal() ?? 0
guard let can = self.tapCashData.getCAN()?.hexString() else {
self.emoney.setTampilRiwayat(false)
self.updateScreen()
self.apduRunner.invalidateSession(msg: "readFailed".localizeString(string: self.langCode!))
return
}
self.emoney.setBalance(balance)
self.emoney.setCardNumber(can)
self.totalLog = self.tapCashData.getTotalRecords()?.hexString().hex2decimal() ?? 0
if (self.totalLog > 10){
self.totalLog = 10
}
debugLog("total log " + String(self.totalLog))
self.getHistory(index: 0)
self.historyRetryCount = 0
self.startHistoryRead()
} else {
self.emoney.setTampilRiwayat(false)
self.updateScreen()
@ -39,29 +47,58 @@ public class TapCashApi : UnifiedNfcApi {
}
})
}
private func startHistoryRead() {
start = 0
riwayatList.removeAll()
getHistory(index: 0)
}
private func retryHistoryRead(reason: String) {
if historyRetryCount < 1 {
historyRetryCount += 1
debugLog("Retrying TapCash history read: \(reason)")
startHistoryRead()
} else {
finalizeHistoryRead()
}
}
private func finalizeHistoryRead() {
self.updateScreen()
self.riwayatList = self.riwayatList.sorted {
($0.getTransationTime() ?? Date.distantPast) > ($1.getTransationTime() ?? Date.distantPast)
}
if (self.riwayatList.count > 0){
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
}
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
}
private func getHistory(index : Int){
let st = String(index).leftPad(with: "0", length: 2)
guard let payload = st.stringToBytes() else {
debugLog("Invalid TapCash history index payload: \(st)")
return
}
//90 32 03 00 01 00 10
let TAPCASH_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_ : st.stringToBytes()!), expectedResponseLength : 16)
let TAPCASH_LOG = NFCISO7816APDU(instructionClass : 0x90, instructionCode : 0x32, p1Parameter : 0x03, p2Parameter : 0x00, data : Data(_ : payload), expectedResponseLength : 16)
debugLog(TAPCASH_LOG.toHexString())
apduRunner.exchangeApdu(apduCommand: TAPCASH_LOG, completionHandler: {response in
if (response.sw1 == 0x90 && response.sw2 == 0x00){
self.addRiwayatTransaksi(data: response.getData().bytes)
} else {
self.retryHistoryRead(reason: "status \(response.sw1)-\(response.sw2) at index \(index)")
return
}
self.start+=1
if (self.start < (self.totalLog)){
self.getHistory(index: self.start)
} else {
self.updateScreen()
self.riwayatList = self.riwayatList.sorted(by: { $0.getTransationTime()?.compare($1.getTransationTime()!) == .orderedDescending })
if (self.riwayatList.count > 0){
self.emoney.setRiwayatList(self.riwayatList)
self.emoney.setTampilRiwayat(true)
}
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
self.finalizeHistoryRead()
}
})
}
@ -73,6 +110,10 @@ public class TapCashApi : UnifiedNfcApi {
}
private func addRiwayatTransaksi(data: [UInt8]) {
guard data.count >= 8 else {
debugLog("TapCash history data too short: \(data.count)")
return
}
let trxType = Array(data[0..<1])
let trxAmount = Array(data[1..<4])
let trxDateTimes = Array(data[4..<8])

View File

@ -21,6 +21,7 @@ extension Dictionary where Key == String, Value == Any {
@available(iOS 13.0, *)
public class UnifiedNfcApi {
var stationMap: [Int: Station] = [:]
var felicaHistoryRetryCount = 0
func parseData() {
stationMap = [
@ -87,7 +88,7 @@ public class UnifiedNfcApi {
var apduRunner = ApduRunner()
public init() {
langCode = Locale.current.languageCode
langCode = Locale.current.languageCode ?? "en"
parseData()
}
@ -124,9 +125,16 @@ public class UnifiedNfcApi {
public func checkFelicaCard(tag: NFCFeliCaTag){
readFelicaCard(tag: tag)
}
private func logFelica(_ message: String) {
_ = message
// print("[FeliCa] \(message)")
// debugLog("[FeliCa] \(message)")
}
func readFelicaCard(tag: NFCFeliCaTag){
let kmt = Emoney()
logFelica("Start read | currentIDm=\(tag.currentIDm.map { String(format: "%02X", $0) }.joined()) | currentSystemCode=\(tag.currentSystemCode.map { String(format: "%02X", $0) }.joined())")
let serviceCode = Data([0x0B, 0x30])
let blockList = Data([0x80, 0x00])
@ -137,21 +145,27 @@ public class UnifiedNfcApi {
) { (status1, status2, blockData, error) in
if let error = error {
self.logFelica("Card info read failed | error=\(error.localizedDescription)")
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
return
}
// Cek status keberhasilan dari kartu (0x00 0x00 berarti sukses)
guard status1 == 0x00 && status2 == 0x00 else {
self.logFelica("Card info read failed | status1=\(status1) | status2=\(status2)")
self.apduRunner.nfcApi?.stopCheckCard(message: "Error Status: \(status1) \(status2)")
return
}
for (index, data) in blockData.enumerated() {
debugLog("Data Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
let hex = data.map { String(format: "%02X", $0) }.joined()
self.logFelica("Card info block \(index) raw=\(hex)")
if let cardNumberString = String(data: data, encoding: .utf8) {
self.logFelica("Card number parsed=\(cardNumberString)")
kmt.setCardLabel("KMT")
kmt.setCardNumber(cardNumberString)
self.readFelicaBalance(tag: tag, kmt: kmt)
} else {
self.logFelica("Card number parse failed for block \(index)")
}
}
}
@ -166,12 +180,18 @@ public class UnifiedNfcApi {
blockList: [blockList]
) { (status1, status2, blockData, error) in
if let error = error {
self.logFelica("Balance read failed | error=\(error.localizedDescription)")
self.apduRunner.nfcApi?.stopCheckCard(message: "Gagal membaca: \(error.localizedDescription)")
return
}
if status1 == 0x00 && status2 == 0x00 {
let cardBalance = [UInt8](blockData[0])
guard let firstBlock = blockData.first, firstBlock.count >= 4 else {
self.logFelica("Balance block missing or too short")
return
}
self.logFelica("Balance raw=\(firstBlock.map { String(format: "%02X", $0) }.joined())")
let cardBalance = [UInt8](firstBlock)
var y: Int = 0
for x in 0..<4 {
y += Int(cardBalance[x]) << (x * 8)
@ -180,12 +200,14 @@ public class UnifiedNfcApi {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "id_ID")
formatter.numberStyle = .decimal
debugLog("balance")
kmt.setBalance(y)
if let balance = formatter.string(from: NSNumber(value: y)) {
debugLog("Saldo: \(balance)") // Hasil contoh: "67.305.985"
self.logFelica("Balance parsed=\(y) | formatted=\(balance)")
}
self.felicaHistoryRetryCount = 0
self.readFelicaCardHistory(tag: tag, kmt: kmt)
} else {
self.logFelica("Balance read failed | status1=\(status1) | status2=\(status2)")
// kmt.setTampilRiwayat(false)
// self.apduRunner.callback?.complete(emoney: kmt)
// self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
@ -210,34 +232,38 @@ public class UnifiedNfcApi {
) { (status1, status2, blockData, error) in
var riwayatList: [RiwayatCard] = []
if let error = error {
debugLog("Error: \(error.localizedDescription)")
self.logFelica("History read failed | error=\(error.localizedDescription)")
self.retryFelicaHistoryRead(tag: tag, kmt: kmt, reason: "error \(error.localizedDescription)")
return
}
if status1 == 0x00 && status2 == 0x00 {
debugLog("Berhasil membaca 15 blok!")
self.logFelica("History read success | blocks=\(blockData.count)")
// blockData akan berisi array of Data, masing-masing 16 byte
for (index, data) in blockData.enumerated() {
let riwayat = RiwayatCard()
var normal = true
let rawHex = data.map { String(format: "%02X", $0) }.joined()
self.logFelica("History block \(index) raw=\(rawHex)")
guard data.count >= 12 else {
self.logFelica("History block \(index) skipped | reason=too_short | count=\(data.count)")
continue
}
let subId = data.subdata(in: 8..<10)
let uid = self.convert(bytes: [UInt8](subId))
debugLog("station: \(uid)")
debugLog("Blok \(index): \(data.map { String(format: "%02X", $0) }.joined())")
self.logFelica("History block \(index) stationId=\(uid)")
if (uid == 0){
normal = false
}
if data.count >= 13 {
let transactionKind = self.felicaTransactionKind(for: [UInt8](data))
let transactionKind = self.felicaTransactionKind(for: [UInt8](data), stationId: uid)
riwayat.setProsesTipe(transactionKind.prosesTipe)
riwayat.setTitle(transactionKind.title.localizeString(string: self.langCode!))
debugLog("signature: \(transactionKind.signature)")
debugLog(transactionKind.logLabel)
self.logFelica("History block \(index) signature=\(transactionKind.signature) | kind=\(transactionKind.logLabel)")
}
if let station = self.stationMap[uid]{
debugLog("station", station.name)
self.logFelica("History block \(index) stationName=\(station.name)")
riwayat.setLocationName(station.name.uppercased(with: .autoupdatingCurrent))
}
// let station = self.stationMap[uid]
@ -247,14 +273,17 @@ public class UnifiedNfcApi {
let subData = data.subdata(in: 0..<4)
let date = self.getDate(data: [UInt8](subData))
riwayat.setTransactionTime(date!)
if let date = self.getDate(data: [UInt8](subData)) {
riwayat.setTransactionTime(date)
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "id_ID") // Format Indonesia
formatter.dateFormat = "dd MMMM yyyy, HH:mm"
let dateString = formatter.string(from: date!)
debugLog("Hasil Konversi: \(dateString)")
if let date = riwayat.getTransationTime() {
let dateString = formatter.string(from: date)
self.logFelica("History block \(index) timestamp=\(dateString)")
}
let amn = data.subdata(in: 4..<8)
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
@ -283,11 +312,10 @@ public class UnifiedNfcApi {
nformatter.locale = Locale(identifier: "id_ID")
nformatter.numberStyle = .decimal
if let balance = nformatter.string(from: NSNumber(value: amount)) {
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
self.logFelica("History block \(index) amount=\(amount) | formatted=\(balance)")
}
debugLog("")
} else {
debugLog("RESKA PARKIR")
self.logFelica("History block \(index) mode=RESKA_PARKIR")
let stringData = data.map { String(format: "%02X", $0) }.joined()
@ -304,9 +332,9 @@ public class UnifiedNfcApi {
let result = outputFormatter.string(from: date)
riwayat.setTransactionTime(date)
debugLog(result) // Hasil: 29-01-2026 16:00:44
self.logFelica("History block \(index) timestamp=\(result)")
} else {
debugLog("Format string tidak cocok")
self.logFelica("History block \(index) timestamp_parse_failed")
}
let amn = data.subdata(in: 8..<12)
let amount = amn.withUnsafeBytes { $0.load(as: Int32.self).bigEndian }
@ -318,26 +346,42 @@ public class UnifiedNfcApi {
nformatter.locale = Locale(identifier: "id_ID")
nformatter.numberStyle = .decimal
if let balance = nformatter.string(from: NSNumber(value: amount)) {
debugLog("amount: \(balance)") // Hasil contoh: "67.305.985"
self.logFelica("History block \(index) amount=\(amount) | formatted=\(balance)")
}
}
debugLog("")
let title = riwayat.getTitle() ?? "-"
let location = riwayat.getLocationName() ?? "-"
self.logFelica("History block \(index) summary | title=\(title) | prosesTipe=\(riwayat.getProsesTipe()) | location=\(location)")
riwayatList.append(riwayat)
}
kmt.setRiwayatList(riwayatList)
kmt.setTampilRiwayat(true)
self.logFelica("History parsed total=\(riwayatList.count)")
self.apduRunner.callback?.complete(emoney: kmt)
self.apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
self.apduRunner.invalidateSession()
} else {
debugLog("Gagal. Status: \(status1), \(status2)")
self.logFelica("History read failed | status1=\(status1) | status2=\(status2)")
self.retryFelicaHistoryRead(tag: tag, kmt: kmt, reason: "status \(status1)-\(status2)")
}
}
}
private func retryFelicaHistoryRead(tag: NFCFeliCaTag, kmt: Emoney, reason: String) {
if felicaHistoryRetryCount < 1 {
felicaHistoryRetryCount += 1
logFelica("Retrying history read | reason=\(reason)")
readFelicaCardHistory(tag: tag, kmt: kmt)
} else {
apduRunner.callback?.complete(emoney: kmt)
apduRunner.sessionEx?.alertMessage = "readFinish".localizeString(string: self.langCode!)
apduRunner.invalidateSession()
}
}
func getDate(data: [UInt8]) -> Date? {
// 1. Tentukan TimeZone Jakarta
let timeZone = TimeZone(identifier: "Asia/Jakarta")!
let timeZone = TimeZone(identifier: "Asia/Jakarta") ?? .current
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
@ -364,7 +408,7 @@ public class UnifiedNfcApi {
func convert(bytes: [UInt8]) -> Int {
switch bytes.count {
case 0:
fatalError("Data kosong")
return 0
case 1:
return Int(bytes[0])
case 2:
@ -379,12 +423,17 @@ public class UnifiedNfcApi {
}
}
private func felicaTransactionKind(for bytes: [UInt8]) -> (prosesTipe: Int, title: String, logLabel: String, signature: String) {
private func felicaTransactionKind(for bytes: [UInt8], stationId: Int) -> (prosesTipe: Int, title: String, logLabel: String, signature: String) {
let signatureBytes = Array(bytes[10...12])
let signature = signatureBytes.map { String(format: "%02X", $0) }.joined(separator: " ")
if stationId == 0 {
return (1, "payment", "Parkir", signature)
}
switch signatureBytes {
case [0x00, 0x02, 0x00],
[0x02, 0x01, 0x00],
[0x03, 0x01, 0x00]:
return (0, "topup", "Topup", signature)
case [0x01, 0x01, 0x01],
@ -392,14 +441,12 @@ public class UnifiedNfcApi {
[0x03, 0x61, 0x01]:
return (1, "payment", "Pembayaran", signature)
default:
let type = bytes[10]
switch type {
let terminalMarker = bytes[12]
switch terminalMarker {
case 0x00:
return (0, "topup", "Topup (fallback)", signature)
return (0, "topup", "Topup (fallback sig[2])", signature)
case 0x01:
return (1, "payment", "Pembayaran (fallback)", signature)
case 0x03:
return (1, "payment", "Pembayaran (fallback 0x03)", signature)
return (1, "payment", "Pembayaran (fallback sig[2])", signature)
default:
return (1, "payment", "Other", signature)
}

View File

@ -25,14 +25,14 @@ class ApduResponse {
}
func getSw1() -> UInt8{
return sw1!
return sw1 ?? 0
}
func getSw2() -> UInt8{
return sw2!
return sw2 ?? 0
}
func getData() -> Data{
return data!
return data ?? Data()
}
}

View File

@ -28,9 +28,9 @@ public class ApduRunner: NSObject, NFCTagReaderSessionDelegate {
}
func startScan() {
let langCode = Locale.current.languageCode
let langCode = Locale.current.languageCode ?? "en"
self.sessionEx = NFCTagReaderSession(pollingOption: [.iso14443, .iso18092], delegate: self)
self.sessionEx?.alertMessage = "scanMessage".localizeString(string: langCode!)
self.sessionEx?.alertMessage = "scanMessage".localizeString(string: langCode)
self.sessionEx?.begin()
}
@ -47,28 +47,31 @@ public class ApduRunner: NSObject, NFCTagReaderSessionDelegate {
debugLog("Nfc Tag???.")
return
}
if case NFCTag.iso7816(_) = tags.first! {
sessionEx?.connect(to: tags.first!) { [self] (error: Error?) in
guard let firstTag = tags.first else {
return
}
if case NFCTag.iso7816(_) = firstTag {
sessionEx?.connect(to: firstTag) { [self] (error: Error?) in
if let err = error {
debugLog("Error connecting to Nfc Tag" + err.localizedDescription)
return
}
debugLog("Nfc Tag is connected.")
if (self.callback != nil){
self.callback!.connected(unifiedNfcApi: self.nfcApi!)
if let callback = self.callback, let nfcApi = self.nfcApi {
callback.connected(unifiedNfcApi: nfcApi)
}
}
} else if case .feliCa(let feliCaTag) = tags.first! {
sessionEx?.connect(to: tags.first!) { [self] (error: Error?) in
} else if case .feliCa(let feliCaTag) = firstTag {
sessionEx?.connect(to: firstTag) { [self] (error: Error?) in
if let err = error {
debugLog("Error connecting to Nfc Tag" + err.localizedDescription)
return
}
// felicaTag = feliCaTag
debugLog("Felica is connected.")
if (self.callback != nil){
if let callback = self.callback, let nfcApi = self.nfcApi {
debugLog("Felica is connected 2.")
self.callback!.felicaConnected(unifiedNfcApi: self.nfcApi!, tag: feliCaTag)
callback.felicaConnected(unifiedNfcApi: nfcApi, tag: feliCaTag)
}
// self.sendFelicaCommand(tag: feliCaTag, session: sessionEx!)
@ -88,7 +91,11 @@ public class ApduRunner: NSObject, NFCTagReaderSessionDelegate {
func exchangeApdu(apduCommand: NFCISO7816APDU, completionHandler: @escaping CompletionHandler) {
if case let NFCTag.iso7816(nfcTag) = self.sessionEx!.connectedTag! {
guard let connectedTag = self.sessionEx?.connectedTag else {
debugLog("No connected NFC tag available")
return
}
if case let NFCTag.iso7816(nfcTag) = connectedTag {
nfcTag.sendCommand(apdu: apduCommand) { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?)
in
let resp = ApduResponse()

View File

@ -20,15 +20,15 @@ class Emoney {
}
func getCardNumber() -> String {
return self.cardNumber!
return self.cardNumber ?? ""
}
func getCardType() -> String {
return self.cardType!
return self.cardType ?? ""
}
func getRiwayatList() -> [RiwayatCard] {
return self.riwayatList!
return self.riwayatList ?? []
}
func isTampilRiwayat() -> Bool {
@ -60,7 +60,7 @@ class Emoney {
}
func getCardLabel() -> String {
return self.cardLabel!
return self.cardLabel ?? ""
}
}

View File

@ -40,7 +40,7 @@ class BrizziSamHelper {
static func mix(_ bArr: [UInt8], _ bArr2: [UInt8]) -> [UInt8] {
guard !bArr2.isEmpty else {
fatalError("empty security key")
return bArr
}
var bArr3 = [UInt8](repeating: 0, count: bArr.count)
@ -82,11 +82,26 @@ class BrizziSamHelper {
}
func generateSamRandom() -> String {
let sam = BrizziSamHelper.mix(((BrizziSamHelper.encrypt(self.keyCard!, self.random)!).hexEncodedString()).hex2byte().bytes, ("0000000000000000").hex2byte().bytes).hexString().subString(from: 0, to: 16)
guard
let keyCard = self.keyCard,
let encryptedCard = BrizziSamHelper.encrypt(keyCard, self.random)?.hexEncodedString()
else {
return ""
}
let sam = BrizziSamHelper.mix(encryptedCard.hex2byte().bytes, ("0000000000000000").hex2byte().bytes).hexString().subString(from: 0, to: 16)
guard sam.count == 16 else {
return ""
}
let sams = sam[sam.index(sam.startIndex, offsetBy: 2)..<sam.index(sam.startIndex, offsetBy: 16)] + sam[sam.startIndex..<sam.index(sam.startIndex, offsetBy: 2)]
let result = (BrizziSamHelper.encrypt((BrizziSamHelper.mix(("1122334455667788").hex2byte().bytes, (self.keyCard!).hex2byte().bytes).hexString()), self.random)!).hexEncodedString().subString(from: 0, to: 16)
guard let encryptedResult = BrizziSamHelper.encrypt((BrizziSamHelper.mix(("1122334455667788").hex2byte().bytes, keyCard.hex2byte().bytes).hexString()), self.random)?.hexEncodedString() else {
return ""
}
let result = encryptedResult.subString(from: 0, to: 16)
return result + (BrizziSamHelper.encrypt((BrizziSamHelper.mix(String(sams).hex2byte().bytes, result.hex2byte().bytes)).hexString(), self.random)!).hexEncodedString()
guard let finalResult = BrizziSamHelper.encrypt((BrizziSamHelper.mix(String(sams).hex2byte().bytes, result.hex2byte().bytes)).hexString(), self.random)?.hexEncodedString() else {
return result
}
return result + finalResult
}
private static func crypt(input: Data, keyData: Data, ivData: Data?, operation: CCOperation) -> Data? {

View File

@ -25,8 +25,8 @@ public class ByteArrayAndHexHelper {
public static func hex(from string: String) -> Data {
.init(stride(from: 0, to: string.count, by: 2).map {
string[string.index(string.startIndex, offsetBy: $0) ... string.index(string.startIndex, offsetBy: $0 + 1)]
}.map {
UInt8($0, radix: 16)!
}.compactMap {
UInt8($0, radix: 16)
})
}

View File

@ -25,19 +25,25 @@ extension StringProtocol {
}
func hex2decimal() -> Int {
Int(self, radix: 16)!
Int(self, radix: 16) ?? 0
}
func hex2bin() -> String {
return String(Int(self, radix: 16)!, radix: 2)
guard let value = Int(self, radix: 16) else {
return ""
}
return String(value, radix: 2)
}
func bin2decimal() -> Int {
return Int(self, radix: 2)!
return Int(self, radix: 2) ?? 0
}
func bin2hex() -> String {
return String(Int(self, radix: 2)!, radix: 16)
guard let value = Int(self, radix: 2) else {
return ""
}
return String(value, radix: 16)
}
func secondComplementsAmount() -> Int{
@ -180,6 +186,9 @@ extension String {
}
func subString(from: Int, to: Int) -> String {
guard from >= 0, to >= from, from <= self.count, to <= self.count else {
return ""
}
let startIndex = self.index(self.startIndex, offsetBy: from)
let endIndex = self.index(self.startIndex, offsetBy: to)
return String(self[startIndex..<endIndex])
@ -213,10 +222,13 @@ extension String {
return bytes
}
func localizeString(string: String) -> String {
let path = Bundle.main.path(forResource: string, ofType: "lproj")
let bundle = Bundle(path: path!)
return NSLocalizedString(self, tableName: nil, bundle: bundle!,
guard
let path = Bundle.main.path(forResource: string, ofType: "lproj"),
let bundle = Bundle(path: path)
else {
return NSLocalizedString(self, comment: "")
}
return NSLocalizedString(self, tableName: nil, bundle: bundle,
value: "", comment: "")
}
}

View File

@ -155,6 +155,10 @@ class TapCashData {
}
private func unpackPurseData(data: [UInt8]) {
guard data.count >= 95 else {
debugLog("TapCash purse data too short: \(data.count)")
return
}
version = Array(data[0..<1])
purseStatus = Array(data[1..<2])
purseBalance = Array(data[2..<5])

View File

@ -23,8 +23,10 @@ class ToastHelper {
toastView.alpha = 0
toastView.translatesAutoresizingMaskIntoConstraints = false
let window = UIApplication.shared.delegate?.window!
window?.addSubview(toastView)
guard let window = UIApplication.shared.delegate?.window ?? nil else {
return
}
window.addSubview(toastView)
let horizontalCenterContraint: NSLayoutConstraint = NSLayoutConstraint(item: toastView, attribute: .centerX, relatedBy: .equal, toItem: window, attribute: .centerX, multiplier: 1, constant: 0)