193 lines
5.7 KiB
Kotlin
193 lines
5.7 KiB
Kotlin
package com.iiyh.emoneyinfo.nfc
|
|
|
|
import android.nfc.tech.IsoDep
|
|
import android.nfc.tech.NfcF
|
|
import com.iiyh.emoneyinfo.util.AppLog
|
|
import java.util.Calendar
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
import java.util.TimeZone
|
|
|
|
private const val NFC_LOG_TAG = "EmoneyInfoNfc"
|
|
|
|
internal data class ApduResponse(
|
|
val data: ByteArray,
|
|
val sw1: Int,
|
|
val sw2: Int
|
|
) {
|
|
fun isSuccess(): Boolean = sw1 == 0x90 && sw2 == 0x00
|
|
fun hasStatus(sw1: Int, sw2: Int): Boolean = this.sw1 == sw1 && this.sw2 == sw2
|
|
}
|
|
|
|
internal fun IsoDep.transceiveSelect(aid: ByteArray): ApduResponse =
|
|
transceiveApdu(cla = 0x00, ins = 0xA4, p1 = 0x04, p2 = 0x00, data = aid)
|
|
|
|
internal fun IsoDep.transceiveApdu(
|
|
cla: Int,
|
|
ins: Int,
|
|
p1: Int,
|
|
p2: Int,
|
|
data: ByteArray? = null,
|
|
le: Int? = null
|
|
): ApduResponse {
|
|
val apdu = mutableListOf(
|
|
cla.toByte(),
|
|
ins.toByte(),
|
|
p1.toByte(),
|
|
p2.toByte()
|
|
)
|
|
if (data != null) {
|
|
apdu += data.size.toByte()
|
|
apdu += data.toList()
|
|
}
|
|
if (le != null) {
|
|
apdu += if (le == 256) 0x00 else (le and 0xFF).toByte()
|
|
}
|
|
|
|
val requestBytes = apdu.toByteArray()
|
|
AppLog.d(
|
|
NFC_LOG_TAG,
|
|
"APDU -> cla=%02X ins=%02X p1=%02X p2=%02X lc=%d le=%s data=%s".format(
|
|
cla and 0xFF,
|
|
ins and 0xFF,
|
|
p1 and 0xFF,
|
|
p2 and 0xFF,
|
|
data?.size ?: 0,
|
|
le?.toString() ?: "-",
|
|
data?.toHex() ?: ""
|
|
)
|
|
)
|
|
val response = try {
|
|
transceive(requestBytes)
|
|
} catch (error: Throwable) {
|
|
AppLog.e(
|
|
NFC_LOG_TAG,
|
|
"APDU !! cla=%02X ins=%02X failed: %s".format(
|
|
cla and 0xFF,
|
|
ins and 0xFF,
|
|
error.message ?: error.javaClass.simpleName
|
|
),
|
|
error
|
|
)
|
|
throw error
|
|
}
|
|
require(response.size >= 2) { "Invalid APDU response" }
|
|
val parsed = ApduResponse(
|
|
data = response.copyOf(response.size - 2),
|
|
sw1 = response[response.size - 2].toInt() and 0xFF,
|
|
sw2 = response[response.size - 1].toInt() and 0xFF
|
|
)
|
|
AppLog.d(
|
|
NFC_LOG_TAG,
|
|
"APDU <- sw=%02X%02X data=%s".format(parsed.sw1, parsed.sw2, parsed.data.toHex())
|
|
)
|
|
return parsed
|
|
}
|
|
|
|
internal fun NfcF.readWithoutEncryption(
|
|
serviceCode: ByteArray,
|
|
blockNumbers: List<Int>
|
|
): List<ByteArray> {
|
|
val packet = mutableListOf<Byte>()
|
|
packet += 0x00
|
|
packet += 0x06
|
|
packet += tag.id.toList()
|
|
packet += 0x01
|
|
packet += serviceCode.toList()
|
|
packet += blockNumbers.size.toByte()
|
|
blockNumbers.forEach { blockNo ->
|
|
packet += 0x80.toByte()
|
|
packet += blockNo.toByte()
|
|
}
|
|
packet[0] = packet.size.toByte()
|
|
|
|
val response = transceive(packet.toByteArray())
|
|
require(response.size >= 13) { "Invalid FeliCa response" }
|
|
val status1 = response[10].toInt() and 0xFF
|
|
val status2 = response[11].toInt() and 0xFF
|
|
require(status1 == 0x00 && status2 == 0x00) {
|
|
"FeliCa status error: $status1/$status2"
|
|
}
|
|
val blockCount = response[12].toInt() and 0xFF
|
|
var offset = 13
|
|
return buildList {
|
|
repeat(blockCount) {
|
|
add(response.copyOfRange(offset, offset + 16))
|
|
offset += 16
|
|
}
|
|
}
|
|
}
|
|
|
|
internal fun ByteArray.toHex(): String = joinToString("") { "%02X".format(it) }
|
|
|
|
internal fun String.hexToBytes(): ByteArray {
|
|
require(length % 2 == 0) { "Hex string must have even length" }
|
|
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
}
|
|
|
|
internal fun String.hexToLong(): Long = if (isBlank()) 0 else toLong(16)
|
|
|
|
internal fun String.hexToIntOrZero(): Int = toIntOrNull(16) ?: 0
|
|
|
|
internal fun String.reverseByteOrderHex(): String =
|
|
chunked(2).reversed().joinToString("")
|
|
|
|
internal fun String.safeSlice(start: Int, end: Int): String {
|
|
if (length <= start) return ""
|
|
return substring(start, minOf(end, length))
|
|
}
|
|
|
|
internal fun String.formatCardNumber(): String =
|
|
chunked(4).joinToString(" ").trim()
|
|
|
|
internal fun ByteArray.rotateLeftBytes(count: Int): ByteArray {
|
|
if (isEmpty()) return this
|
|
val shift = count % size
|
|
return copyOfRange(shift, size) + copyOfRange(0, shift)
|
|
}
|
|
|
|
internal fun String.twosComplementHexToLong(): Long {
|
|
val bits = length * 4
|
|
val value = hexToLong()
|
|
val signBit = 1L shl (bits - 1)
|
|
return if ((value and signBit) == 0L) value else value - (1L shl bits)
|
|
}
|
|
|
|
internal fun ByteArray.bigEndianLong(): Long =
|
|
fold(0L) { acc, byte -> (acc shl 8) or (byte.toInt() and 0xFF).toLong() }
|
|
|
|
internal fun ByteArray.littleEndianLong(): Long =
|
|
reversedArray().bigEndianLong()
|
|
|
|
internal fun parseDdmmyyHhmmss(datePart: String, timePart: String): Date? {
|
|
return runCatching {
|
|
val raw = datePart + timePart
|
|
java.text.SimpleDateFormat("ddMMyyHHmmss", Locale.US).parse(raw)
|
|
}.getOrNull()
|
|
}
|
|
|
|
internal fun julianSecondsFrom1995(seconds: Long): Date {
|
|
val calendar = Calendar.getInstance().apply {
|
|
timeZone = TimeZone.getTimeZone("UTC")
|
|
set(1995, Calendar.JANUARY, 1, 0, 0, 0)
|
|
set(Calendar.MILLISECOND, 0)
|
|
}
|
|
return Date(calendar.timeInMillis + (seconds * 1000))
|
|
}
|
|
|
|
internal fun kmtSecondsFrom2000(seconds: Long): Date {
|
|
val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply {
|
|
set(2000, Calendar.JANUARY, 1, 7, 0, 0)
|
|
set(Calendar.MILLISECOND, 0)
|
|
}
|
|
return Date(calendar.timeInMillis + (seconds * 1000))
|
|
}
|
|
|
|
internal fun flazzSecondsFrom1980(seconds: Long): Date {
|
|
val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply {
|
|
set(1980, Calendar.JANUARY, 1, 0, 0, 0)
|
|
set(Calendar.MILLISECOND, 0)
|
|
}
|
|
return Date(calendar.timeInMillis + (seconds * 1000))
|
|
}
|