Improve NFC history readers and prepare production build
This commit is contained in:
@ -623,7 +623,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Emoney Info/Emoney Info.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 13;
|
||||
DEVELOPMENT_TEAM = 6S5573WXX4;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -643,7 +643,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.iiyh.emoneyinfo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -666,7 +666,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Emoney Info/Emoney Info.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 13;
|
||||
DEVELOPMENT_TEAM = 6S5573WXX4;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -686,7 +686,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.iiyh.emoneyinfo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 ?? ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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? {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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: "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user