Updated on 2026-08-14
This commit is contained in:
parent
7e7e3254f3
commit
0c419c3441
9 changed files with 167 additions and 17 deletions
|
|
@ -183,7 +183,7 @@ class Card(
|
|||
/**
|
||||
* Current status of the card.
|
||||
*/
|
||||
val status: CardStatus,
|
||||
val status: CardStatus?,
|
||||
|
||||
/**
|
||||
* Version of Tangem COS.
|
||||
|
|
@ -316,9 +316,9 @@ class ReadCommand : CommandSerializer<Card>() {
|
|||
val tlvMapper = TlvMapper(tlvData)
|
||||
|
||||
Card(
|
||||
cardId = tlvMapper.map(TlvTag.CardId),
|
||||
manufacturerName = tlvMapper.map(TlvTag.ManufactureId),
|
||||
status = tlvMapper.map(TlvTag.Status),
|
||||
cardId = tlvMapper.mapOptional(TlvTag.CardId) ?: "",
|
||||
manufacturerName = tlvMapper.mapOptional(TlvTag.ManufactureId) ?: "",
|
||||
status = tlvMapper.mapOptional(TlvTag.Status),
|
||||
|
||||
firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware),
|
||||
cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey),
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ class Tlv {
|
|||
}
|
||||
|
||||
|
||||
fun deserialize(mData: ByteArray): List<Tlv>? {
|
||||
fun deserialize(data: ByteArray, nfcV: Boolean = false): List<Tlv>? {
|
||||
val tlvList = mutableListOf<Tlv>()
|
||||
val stream = ByteArrayInputStream(mData)
|
||||
val stream = ByteArrayInputStream(data)
|
||||
var tlv: Tlv?
|
||||
do {
|
||||
try {
|
||||
|
|
@ -63,7 +63,7 @@ class Tlv {
|
|||
if (tlv != null) tlvList.add(tlv)
|
||||
} catch (e: IOException) {
|
||||
Log.e(this::class.java.simpleName,"TLVError: " + e.message)
|
||||
return null
|
||||
if (nfcV) break else return null
|
||||
}
|
||||
|
||||
} while (tlv != null)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CardStatus
|
||||
import com.tangem.commands.CheckWalletCommand
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
||||
|
|
@ -41,6 +38,10 @@ internal class ScanTask : Task<ScanEvent>() {
|
|||
completeNfcSession(true, TaskError.MissingPreflightRead())
|
||||
callback(TaskEvent.Completion(TaskError.MissingPreflightRead()))
|
||||
|
||||
} else if (currentCard.cardData?.productMask == ProductMask.Tag) {
|
||||
completeNfcSession()
|
||||
callback(TaskEvent.Completion())
|
||||
|
||||
} else if (currentCard.status != CardStatus.Loaded) {
|
||||
completeNfcSession()
|
||||
callback(TaskEvent.Completion())
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ abstract class Task<T> {
|
|||
is CompletionResult.Success -> {
|
||||
|
||||
val receivedCardId = readResult.data.cardId
|
||||
securityDelayDuration = readResult.data.pauseBeforePin2 ?: 0
|
||||
|
||||
if (environment.cardId != null && environment.cardId != receivedCardId) {
|
||||
completeNfcSession(true, TaskError.WrongCard())
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class TlvTest {
|
|||
assertThat(tlvs)
|
||||
.isNotEmpty()
|
||||
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pinExpected = "000000".calculateSha256()
|
||||
|
||||
assertThat(pin)
|
||||
|
|
@ -72,7 +72,7 @@ class TlvTest {
|
|||
assertThat(tlvs)
|
||||
.isNotEmpty()
|
||||
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pinExpected = "000000".calculateSha256()
|
||||
assertThat(pin)
|
||||
.isEqualTo(pinExpected)
|
||||
|
|
@ -92,12 +92,20 @@ class TlvTest {
|
|||
fun `Bytes to TLVs, wrong values`() {
|
||||
val bytes = byteArrayOf(0)
|
||||
val tlvs = Tlv.deserialize(bytes)
|
||||
assertThat(tlvs)
|
||||
.isNull()
|
||||
assertThat(tlvs)
|
||||
.isNull()
|
||||
|
||||
val bytes1 = byteArrayOf(0, 0, 0, 0, 0, 0, 0)
|
||||
val tlvs1 = Tlv.deserialize(bytes1)
|
||||
assertThat(tlvs1)
|
||||
.isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse Slix tag response`() {
|
||||
val response = "03ff010f91010b550474616e67656d2e636f6d140f11616e64726f69642e636f6d3a706b67636f6d2e74616e67656d2e77616c6c65745411c974616e67656d2e636f6d3a77616c6c657490000c618102ffff8a0102820407e40109830b54414e47454d2053444b008403584c4d86400e71c1f060387029688254320b90abeae471bcafbbe8ea3880903bdb8d1cc389d032b982e1ffd7ef49e66f1780123b763dd2f3a9a9494eb0fad4ae8cf306672c60207c967a51077c14fc49d867f23b8d0eaf60cad479a56587e894571b7fb33690176140345fbe53f5be0ec871e91c317cde2bd0396d47e4b945c138c153b0271f636a73cf531df1bc54ac4fcdbce42f81b40d58e0265d34e28121a4c50fdfe329a97f6000fe000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
val tlvs = Tlv.deserialize(response.hexToBytes(), true)
|
||||
assertThat(tlvs)
|
||||
.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,5 +3,6 @@
|
|||
<tech-list>
|
||||
<tech>android.nfc.tech.IsoDep</tech>
|
||||
<tech>android.nfc.tech.Ndef</tech>
|
||||
<tech>android.nfc.tech.NfcV</tech>
|
||||
</tech-list>
|
||||
</resources>
|
||||
|
|
@ -87,6 +87,7 @@ class NfcManager : NfcAdapter.ReaderCallback {
|
|||
companion object {
|
||||
// reader mode flags: listen for type A (not B), skipping ndef check
|
||||
private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or
|
||||
NfcAdapter.FLAG_READER_NFC_V or
|
||||
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or
|
||||
NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tangem_sdk_new.nfc
|
|||
import android.nfc.Tag
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.nfc.tech.NfcV
|
||||
import com.tangem.CardReader
|
||||
import com.tangem.Log
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -41,8 +42,8 @@ class NfcReader : CardReader {
|
|||
}
|
||||
}
|
||||
|
||||
var data: ByteArray? = null
|
||||
var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
|
||||
private var data: ByteArray? = null
|
||||
private var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
|
||||
|
||||
override fun openSession() {
|
||||
readingActive = true
|
||||
|
|
@ -90,6 +91,7 @@ class NfcReader : CardReader {
|
|||
}
|
||||
|
||||
fun onTagDiscovered(tag: Tag?) {
|
||||
NfcV.get(tag)?.let { onNfcVDiscovered(it) }
|
||||
isoDep = IsoDep.get(tag)
|
||||
transceiveData()
|
||||
}
|
||||
|
|
@ -103,4 +105,17 @@ class NfcReader : CardReader {
|
|||
Log.i(this::class.simpleName!!, "Nfc session is started")
|
||||
}
|
||||
|
||||
private fun onNfcVDiscovered(nfcV: NfcV) {
|
||||
val response = SlixTagReader().transceive(nfcV)
|
||||
when (response) {
|
||||
is SlixReadResult.Failure -> {
|
||||
callback?.invoke(CompletionResult.Failure(
|
||||
TaskError.ErrorProcessingCommand(response.exception.message))
|
||||
)
|
||||
}
|
||||
is SlixReadResult.Success -> {
|
||||
callback?.invoke(CompletionResult.Success(ResponseApdu(response.data)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.tangem_sdk_new.nfc
|
||||
|
||||
import android.nfc.NdefMessage
|
||||
import android.nfc.NdefRecord
|
||||
import android.nfc.tech.NfcV
|
||||
import com.tangem.common.apdu.StatusWord
|
||||
import com.tangem.common.extensions.toByteArray
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
class SlixTagReader() {
|
||||
|
||||
private lateinit var nfcV: NfcV
|
||||
|
||||
fun transceive(nfcV: NfcV): SlixReadResult {
|
||||
this.nfcV = nfcV
|
||||
if (!nfcV.isConnected) {
|
||||
try {
|
||||
nfcV.connect()
|
||||
} catch (e: Exception) {
|
||||
return SlixReadResult.Failure(e)
|
||||
}
|
||||
}
|
||||
return try {
|
||||
runRead()
|
||||
} catch (e: Exception) {
|
||||
nfcV.close()
|
||||
SlixReadResult.Failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runRead(): SlixReadResult {
|
||||
val ndefMessage = runReadNDEF()
|
||||
val records: Array<NdefRecord> = ndefMessage.records
|
||||
for (record in records) {
|
||||
if (record.toUri() != null && record.toUri().toString() == "vnd.android.nfc://ext/tangem.com:wallet") {
|
||||
val payload = record.payload
|
||||
val status = StatusWord.ProcessCompleted.code.toByteArray()
|
||||
val data = payload.copyOfRange(2, payload.size) + status
|
||||
nfcV.close()
|
||||
return SlixReadResult.Success(data)
|
||||
}
|
||||
}
|
||||
return SlixReadResult.Failure(Exception("No relevant data was found."))
|
||||
}
|
||||
|
||||
private fun runReadNDEF(): NdefMessage {
|
||||
val answerCC = readSingleBlock(0x00)
|
||||
|
||||
if (answerCC.size != 4 || answerCC[0].toInt() != 0xE1 || ((answerCC[1].toInt() and 0xF0) != 0x40)) {
|
||||
|
||||
} else {
|
||||
throw Exception("Failed! Invalid CC read " + answerCC.toHexString())
|
||||
}
|
||||
if ((answerCC[3].toInt() and 0x01) != 0x01) {
|
||||
throw Exception("Multiple block read unsupported!")
|
||||
}
|
||||
val areaSize = 8 * answerCC[2]
|
||||
val blocksCount = areaSize / 4
|
||||
|
||||
val areaBuf = readMultipleBlocks(1, blocksCount)
|
||||
|
||||
val tlvNdef = TlvMapper(Tlv.deserialize(areaBuf, true) ?: listOf())
|
||||
|
||||
return NdefMessage(tlvNdef.map<ByteArray>(TlvTag.CardPublicKey))
|
||||
}
|
||||
|
||||
private fun readSingleBlock(blockNo: Int): ByteArray {
|
||||
return performRead(0x20, blockNo)!!
|
||||
}
|
||||
|
||||
private fun readMultipleBlocks(startBlock: Int, blocksCount: Int): ByteArray {
|
||||
val resultBuf = ByteArrayOutputStream()
|
||||
val maxBlocksAtOnce = 32
|
||||
var blocksRemaining = blocksCount
|
||||
var firstBlockToRead = startBlock
|
||||
while (blocksRemaining > 0) {
|
||||
val blocksToRead = if (blocksRemaining > maxBlocksAtOnce) {
|
||||
maxBlocksAtOnce
|
||||
} else {
|
||||
blocksRemaining
|
||||
}
|
||||
val blocks = performRead(0x23.toByte(), firstBlockToRead, blocksToRead - 1)
|
||||
blocksRemaining -= blocksToRead
|
||||
firstBlockToRead += blocksToRead
|
||||
resultBuf.write(blocks!!)
|
||||
}
|
||||
return resultBuf.toByteArray()
|
||||
}
|
||||
|
||||
private fun performRead(cmd: Byte, p1: Int?, p2: Int? = null, params: ByteArray? = null): ByteArray? {
|
||||
val command: ByteArray
|
||||
val res: ByteArray?
|
||||
val os = ByteArrayOutputStream()
|
||||
os.write(REQ_FLAG.toInt())
|
||||
os.write(cmd.toInt())
|
||||
p1?.let { os.write(p1) }
|
||||
p2?.let { os.write(it) }
|
||||
params?.let { os.write(params, 0, params.size) }
|
||||
command = os.toByteArray()
|
||||
if (!nfcV.isConnected) throw IOException("Connection lost")
|
||||
res = nfcV.transceive(command)
|
||||
val errorCode = res[0].toInt()
|
||||
if (errorCode != 0) {
|
||||
throw IOException("Error! Code: " + String.format("0x%02x", errorCode))
|
||||
}
|
||||
return res.copyOfRange(1, res.size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// iso15693 flags
|
||||
private const val REQ_FLAG: Byte = 0x02
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SlixReadResult {
|
||||
data class Success(val data: ByteArray) : SlixReadResult()
|
||||
data class Failure(val exception: Exception) : SlixReadResult()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue