Updated on 2026-08-14
This commit is contained in:
parent
0355c34514
commit
300e100a23
30 changed files with 264 additions and 275 deletions
|
|
@ -1,4 +1,4 @@
|
|||
ext.versions = [
|
||||
kotlin : '1.3.72',
|
||||
build_gradle: '3.6.3',
|
||||
build_gradle: '4.0.0',
|
||||
]
|
||||
|
|
|
|||
0
gradlew
vendored
Executable file → Normal file
0
gradlew
vendored
Executable file → Normal file
|
|
@ -16,6 +16,15 @@ interface CardReader {
|
|||
val tag: BroadcastChannel<TagType?>
|
||||
var scope: CoroutineScope?
|
||||
|
||||
/**
|
||||
* Sends data to the card and receives the reply in an asynchronous way using coroutines.
|
||||
*
|
||||
* @param apdu Data to be sent. [CommandApdu] serializes it to a [ByteArray]
|
||||
* @param callback Returns response from the card,
|
||||
* [ResponseApdu] Allows to convert raw data to [Tlv]
|
||||
*/
|
||||
suspend fun transceiveApdu(apdu: CommandApdu): CompletionResult<ResponseApdu>
|
||||
|
||||
/**
|
||||
* Sends data to the card and receives the reply.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -12,14 +12,8 @@ import com.tangem.crypto.EncryptionHelper
|
|||
import com.tangem.crypto.FastEncryptionHelper
|
||||
import com.tangem.crypto.StrongEncryptionHelper
|
||||
import com.tangem.crypto.pbkdf2Hash
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
|
||||
|
|
@ -61,11 +55,11 @@ enum class TagType {
|
|||
* If null, a default header and text body will be used.
|
||||
*/
|
||||
class CardSession(
|
||||
val environment: SessionEnvironment,
|
||||
private val reader: CardReader,
|
||||
val viewDelegate: SessionViewDelegate,
|
||||
private var cardId: String? = null,
|
||||
private val initialMessage: Message? = null
|
||||
val environment: SessionEnvironment,
|
||||
private val reader: CardReader,
|
||||
val viewDelegate: SessionViewDelegate,
|
||||
private var cardId: String? = null,
|
||||
private val initialMessage: Message? = null
|
||||
) {
|
||||
|
||||
var connectedTag: TagType? = null
|
||||
|
|
@ -75,7 +69,9 @@ class CardSession(
|
|||
*/
|
||||
private var state = CardSessionState.Inactive
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { _, ex ->
|
||||
throw ex
|
||||
}
|
||||
|
||||
private val tag = this.javaClass.simpleName
|
||||
|
||||
|
|
@ -86,17 +82,14 @@ class CardSession(
|
|||
* @param callback will be triggered with a [CompletionResult] of a session.
|
||||
*/
|
||||
fun <T : CardSessionRunnable<R>, R : CommandResponse> startWithRunnable(
|
||||
runnable: T, callback: (result: CompletionResult<R>) -> Unit) {
|
||||
runnable: T, callback: (result: CompletionResult<R>) -> Unit
|
||||
) {
|
||||
|
||||
start(runnable.performPreflightRead) { session, error ->
|
||||
if (error != null) {
|
||||
callback(CompletionResult.Failure(error))
|
||||
return@start
|
||||
}
|
||||
if (runnable is ReadCommand) {
|
||||
callback(CompletionResult.Success(environment.card as R))
|
||||
return@start
|
||||
}
|
||||
|
||||
runnable.run(this) { result ->
|
||||
when (result) {
|
||||
|
|
@ -121,8 +114,10 @@ class CardSession(
|
|||
* Starts a card session and performs preflight [ReadCommand].
|
||||
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
|
||||
*/
|
||||
fun start(performPreflightRead: Boolean = true,
|
||||
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
|
||||
fun start(
|
||||
performPreflightRead: Boolean = true,
|
||||
callback: (session: CardSession, error: TangemSdkError?) -> Unit
|
||||
) {
|
||||
|
||||
if (state != CardSessionState.Inactive) {
|
||||
callback(this, TangemSdkError.Busy())
|
||||
|
|
@ -133,21 +128,21 @@ class CardSession(
|
|||
|
||||
scope.launch {
|
||||
reader.tag
|
||||
.asFlow()
|
||||
.collect { tagType ->
|
||||
if (tagType == null && connectedTag != null) {
|
||||
handleTagLost()
|
||||
} else if (tagType != null) {
|
||||
connectedTag = tagType
|
||||
viewDelegate.onTagConnected()
|
||||
.asFlow()
|
||||
.collect { tagType ->
|
||||
if (tagType == null && connectedTag != null) {
|
||||
handleTagLost()
|
||||
} else if (tagType != null) {
|
||||
connectedTag = tagType
|
||||
viewDelegate.onTagConnected()
|
||||
|
||||
if (tagType == TagType.Nfc && performPreflightRead) {
|
||||
preflightCheck(callback)
|
||||
} else {
|
||||
callback(this@CardSession, null)
|
||||
}
|
||||
if (tagType == TagType.Nfc && performPreflightRead) {
|
||||
preflightCheck(callback)
|
||||
} else {
|
||||
callback(this@CardSession, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.scope = scope
|
||||
reader.startSession()
|
||||
|
|
@ -164,15 +159,8 @@ class CardSession(
|
|||
readCommand.run(this) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
tryHandleError(result.error) { handleErrorResult ->
|
||||
when (handleErrorResult) {
|
||||
is CompletionResult.Success -> preflightCheck(callback)
|
||||
is CompletionResult.Failure -> {
|
||||
stopWithError(result.error)
|
||||
callback(this, result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
stopWithError(result.error)
|
||||
callback(this, result.error)
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
val receivedCardId = result.data.cardId
|
||||
|
|
@ -230,65 +218,53 @@ class CardSession(
|
|||
|
||||
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
|
||||
val subscription = reader.tag.openSubscription()
|
||||
|
||||
scope.launch {
|
||||
subscription.consumeAsFlow()
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
reader.transceiveApdu(apdu) { result ->
|
||||
subscription.cancel()
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryHandleError(
|
||||
error: TangemSdkError, callback: (result: CompletionResult<Boolean>) -> Unit) {
|
||||
|
||||
when (error) {
|
||||
is TangemSdkError.NeedEncryption -> {
|
||||
Log.i(tag, "Establishing encryption")
|
||||
when (environment.encryptionMode) {
|
||||
EncryptionMode.NONE -> {
|
||||
environment.encryptionKey = null
|
||||
environment.encryptionMode = EncryptionMode.FAST
|
||||
}
|
||||
EncryptionMode.FAST -> {
|
||||
environment.encryptionKey = null
|
||||
environment.encryptionMode = EncryptionMode.STRONG
|
||||
}
|
||||
EncryptionMode.STRONG -> {
|
||||
Log.e(tag, "Encryption doesn't work")
|
||||
callback(CompletionResult.Failure(TangemSdkError.NeedEncryption()))
|
||||
}
|
||||
.filterNotNull()
|
||||
.map { establishEncryption() }
|
||||
.map { apdu.encrypt(environment.encryptionMode, environment.encryptionKey) }
|
||||
.map { encryptedApdu -> reader.transceiveApdu(encryptedApdu) }
|
||||
.catch { error ->
|
||||
if (error is TangemSdkError) callback(
|
||||
CompletionResult.Failure(
|
||||
error
|
||||
)
|
||||
)
|
||||
}
|
||||
.collect { result ->
|
||||
subscription.cancel()
|
||||
callback(result)
|
||||
}
|
||||
return establishEncryption(callback)
|
||||
}
|
||||
else -> callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun establishEncryption(callback: (result: CompletionResult<Boolean>) -> Unit) {
|
||||
private suspend fun establishEncryption(): CompletionResult<Boolean> {
|
||||
if (environment.encryptionKey != null) return CompletionResult.Success(true)
|
||||
val encryptionHelper: EncryptionHelper =
|
||||
if (environment.encryptionMode == EncryptionMode.STRONG) {
|
||||
StrongEncryptionHelper()
|
||||
} else {
|
||||
FastEncryptionHelper()
|
||||
}
|
||||
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
|
||||
openSesssionCommand.run(this) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val uid = result.data.uid
|
||||
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
|
||||
val secret = encryptionHelper.generateSecret(result.data.sessionKeyB)
|
||||
val sessionKey = (secret + protocolKey).calculateSha256()
|
||||
environment.encryptionKey = sessionKey
|
||||
callback(CompletionResult.Success(true))
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
when (environment.encryptionMode) {
|
||||
EncryptionMode.NONE -> return CompletionResult.Success(true)
|
||||
EncryptionMode.FAST -> FastEncryptionHelper()
|
||||
EncryptionMode.STRONG -> StrongEncryptionHelper()
|
||||
}
|
||||
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
|
||||
val apdu = openSesssionCommand.serialize(environment)
|
||||
|
||||
val response = reader.transceiveApdu(apdu)
|
||||
when (response) {
|
||||
is CompletionResult.Success -> {
|
||||
val result = try {
|
||||
openSesssionCommand.deserialize(environment, response.data)
|
||||
} catch (error: TangemSdkError) {
|
||||
return CompletionResult.Failure(error)
|
||||
}
|
||||
val uid = result.uid
|
||||
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
|
||||
val secret = encryptionHelper.generateSecret(result.sessionKeyB)
|
||||
val sessionKey = (secret + protocolKey).calculateSha256()
|
||||
environment.encryptionKey = sessionKey
|
||||
return CompletionResult.Success(true)
|
||||
}
|
||||
is CompletionResult.Failure -> return CompletionResult.Failure(response.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ data class SessionEnvironment(
|
|||
/**
|
||||
* All possible encryption modes.
|
||||
*/
|
||||
enum class EncryptionMode(val code: Byte) {
|
||||
enum class EncryptionMode(val code: Int) {
|
||||
NONE(0x0),
|
||||
FAST(0x1),
|
||||
STRONG(0x2)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.tangem.commands.personalization.entities.Acquirer
|
|||
import com.tangem.commands.personalization.entities.CardConfig
|
||||
import com.tangem.commands.personalization.entities.Issuer
|
||||
import com.tangem.commands.personalization.entities.Manufacturer
|
||||
import com.tangem.commands.verifycard.VerifyCardCommand
|
||||
import com.tangem.commands.verifycard.VerifyCardResponse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.TerminalKeysService
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
|
@ -294,6 +296,11 @@ class TangemSdk(
|
|||
startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback)
|
||||
}
|
||||
|
||||
fun verify(cardId: String? = null, online: Boolean = true, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<VerifyCardResponse>) -> Unit) {
|
||||
startSessionWithRunnable(VerifyCardCommand(online), cardId, initialMessage, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Command available on SDK cards only
|
||||
*
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
|
|||
class DecodingFailedMissingTag : TangemSdkError(20005)
|
||||
class DecodingFailedTypeMismatch : TangemSdkError(20006)
|
||||
class DecodingFailed : TangemSdkError(20007)
|
||||
class InvalidResponse : TangemSdkError(20008)
|
||||
|
||||
/**
|
||||
* This error is returned when unknown [StatusWord] is received from a card.
|
||||
|
|
|
|||
|
|
@ -90,15 +90,11 @@ class CheckWalletCommand(
|
|||
tlvBuilder.append(TlvTag.Pin, environment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
|
||||
tlvBuilder.append(TlvTag.Challenge, challenge)
|
||||
return CommandApdu(
|
||||
Instruction.CheckWallet, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CheckWalletResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return CheckWalletResponse(
|
||||
|
|
|
|||
|
|
@ -2,13 +2,31 @@ package com.tangem.commands
|
|||
|
||||
import com.tangem.*
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.apdu.StatusWord
|
||||
import com.tangem.common.apdu.toTangemSdkError
|
||||
import com.tangem.common.apdu.*
|
||||
import com.tangem.common.extensions.toInt
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
|
||||
|
||||
interface ApduSerializable<T : CommandResponse> {
|
||||
/**
|
||||
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
|
||||
* then creates [CommandApdu] with this data.
|
||||
* @param environment [SessionEnvironment] of the current card
|
||||
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
|
||||
* that can be sent to a Tangem card
|
||||
*/
|
||||
fun serialize(environment: SessionEnvironment): CommandApdu
|
||||
|
||||
/**
|
||||
* Deserializes data received from a card and stored in [ResponseApdu]
|
||||
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
|
||||
* @param environment [SessionEnvironment] of the current card.
|
||||
* @param apdu received data.
|
||||
* @return Card response converted to a [CommandResponse] of a type [T]
|
||||
*/
|
||||
fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic interface for a parsed response from [Command].
|
||||
*/
|
||||
|
|
@ -17,28 +35,10 @@ interface CommandResponse
|
|||
/**
|
||||
* Basic class for Tangem card commands
|
||||
*/
|
||||
abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
|
||||
abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRunnable<T> {
|
||||
|
||||
override val performPreflightRead: Boolean = true
|
||||
|
||||
/**
|
||||
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
|
||||
* then creates [CommandApdu] with this data.
|
||||
* @param environment [SessionEnvironment] of the current card
|
||||
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
|
||||
* that can be sent to a Tangem card
|
||||
*/
|
||||
abstract fun serialize(environment: SessionEnvironment): CommandApdu
|
||||
|
||||
/**
|
||||
* Deserializes data received from a card and stored in [ResponseApdu]
|
||||
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
|
||||
* @param environment [SessionEnvironment] of the current card.
|
||||
* @param apdu received data.
|
||||
* @return Card response converted to a [CommandResponse] of a type [T]
|
||||
*/
|
||||
abstract fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
|
||||
Log.i("Command", "Initializing ${this::class.java.simpleName}")
|
||||
if (session.environment.handleErrors) {
|
||||
|
|
@ -52,14 +52,18 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
|
|||
}
|
||||
}
|
||||
|
||||
open fun performPreCheck(session: CardSession,
|
||||
callback: (result: CompletionResult<T>) -> Unit): Boolean {
|
||||
open fun performPreCheck(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<T>) -> Unit
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun performAfterCheck(session: CardSession,
|
||||
result: CompletionResult<T>,
|
||||
callback: (result: CompletionResult<T>) -> Unit): Boolean {
|
||||
open fun performAfterCheck(
|
||||
session: CardSession,
|
||||
result: CompletionResult<T>,
|
||||
callback: (result: CompletionResult<T>) -> Unit
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -80,25 +84,63 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
|
|||
}
|
||||
}
|
||||
|
||||
private fun transceiveApdu(apdu: CommandApdu, session: CardSession, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
|
||||
session.send(apdu) { result ->
|
||||
private fun transceiveApdu(
|
||||
apdu: CommandApdu,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ResponseApdu>) -> Unit
|
||||
) {
|
||||
Log.i(this::class.simpleName!!, "transieve: ${Instruction.byCode(apdu.ins)}")
|
||||
|
||||
session.send(apdu) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val responseApdu = result.data
|
||||
|
||||
when (responseApdu.statusWord) {
|
||||
StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged
|
||||
-> callback(CompletionResult.Success(responseApdu))
|
||||
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
|
||||
StatusWord.Pin2Changed, StatusWord.PinsChanged -> {
|
||||
try {
|
||||
val decryptedResponseApdu =
|
||||
responseApdu.decrypt(session.environment.encryptionKey)
|
||||
callback(CompletionResult.Success(decryptedResponseApdu))
|
||||
} catch (error: TangemSdkError) {
|
||||
callback(CompletionResult.Failure(error))
|
||||
}
|
||||
}
|
||||
StatusWord.NeedPause -> {
|
||||
// NeedPause is returned from the card whenever security delay is triggered.
|
||||
val remainingTime = deserializeSecurityDelay(responseApdu, session.environment)
|
||||
val remainingTime =
|
||||
deserializeSecurityDelay(responseApdu)
|
||||
if (remainingTime != null) {
|
||||
session.viewDelegate.onSecurityDelay(
|
||||
remainingTime,
|
||||
session.environment.card?.pauseBeforePin2 ?: 0)
|
||||
remainingTime,
|
||||
session.environment.card?.pauseBeforePin2 ?: 0
|
||||
)
|
||||
}
|
||||
Log.i(
|
||||
this::class.simpleName!!,
|
||||
"Nfc command ${this::class.simpleName!!} " +
|
||||
"triggered security delay of $remainingTime milliseconds"
|
||||
)
|
||||
transceiveApdu(apdu, session, callback)
|
||||
}
|
||||
StatusWord.NeedEncryption -> {
|
||||
Log.i(this::class.simpleName!!, "Establishing encryption")
|
||||
when (session.environment.encryptionMode) {
|
||||
EncryptionMode.NONE -> {
|
||||
session.environment.encryptionKey = null
|
||||
session.environment.encryptionMode = EncryptionMode.FAST
|
||||
}
|
||||
EncryptionMode.FAST -> {
|
||||
session.environment.encryptionKey = null
|
||||
session.environment.encryptionMode = EncryptionMode.STRONG
|
||||
}
|
||||
EncryptionMode.STRONG -> {
|
||||
Log.e(this::class.simpleName!!, "Encryption doesn't work")
|
||||
callback(CompletionResult.Failure(TangemSdkError.NeedEncryption()))
|
||||
return@send
|
||||
}
|
||||
}
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${this::class.simpleName!!} " +
|
||||
"triggered security delay of $remainingTime milliseconds")
|
||||
transceiveApdu(apdu, session, callback)
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -126,7 +168,9 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
|
|||
*
|
||||
* @return Remaining security delay in milliseconds.
|
||||
*/
|
||||
private fun deserializeSecurityDelay(responseApdu: ResponseApdu, environment: SessionEnvironment): Int? {
|
||||
private fun deserializeSecurityDelay(
|
||||
responseApdu: ResponseApdu
|
||||
): Int? {
|
||||
val tlv = responseApdu.getTlvData()
|
||||
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,14 +80,11 @@ class CreateWalletCommand : Command<CreateWalletResponse>() {
|
|||
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
|
||||
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
|
||||
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
|
||||
return CommandApdu(
|
||||
Instruction.CreateWallet, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CreateWalletResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
val tlvData = apdu.getTlvData()
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import com.tangem.common.tlv.TlvDecoder
|
|||
import com.tangem.common.tlv.TlvTag
|
||||
|
||||
class OpenSessionResponse(
|
||||
val sessionKeyB: ByteArray,
|
||||
val uid: ByteArray
|
||||
val sessionKeyB: ByteArray,
|
||||
val uid: ByteArray
|
||||
) : CommandResponse
|
||||
|
||||
/**
|
||||
|
|
@ -25,19 +25,21 @@ class OpenSessionCommand(private val sessionKeyA: ByteArray) : Command<OpenSessi
|
|||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)
|
||||
return CommandApdu(
|
||||
Instruction.OpenSession, tlvBuilder.serialize(),
|
||||
encryptionMode = environment.encryptionMode
|
||||
Instruction.OpenSession.code, tlvBuilder.serialize(), 0, environment.encryptionMode.code
|
||||
)
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): OpenSessionResponse {
|
||||
override fun deserialize(
|
||||
environment: SessionEnvironment,
|
||||
apdu: ResponseApdu
|
||||
): OpenSessionResponse {
|
||||
val tlvData = apdu.getTlvData()
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return OpenSessionResponse(
|
||||
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
|
||||
uid = decoder.decode(TlvTag.Uid)
|
||||
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
|
||||
uid = decoder.decode(TlvTag.Uid)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,15 +67,11 @@ class PurgeWalletCommand : Command<PurgeWalletResponse>() {
|
|||
tlvBuilder.append(TlvTag.Pin, environment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
|
||||
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
|
||||
return CommandApdu(
|
||||
Instruction.PurgeWallet, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): PurgeWalletResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return PurgeWalletResponse(
|
||||
|
|
|
|||
|
|
@ -390,13 +390,10 @@ class ReadCommand : Command<Card>() {
|
|||
*/
|
||||
tlvBuilder.append(TlvTag.Pin, environment.pin1)
|
||||
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
|
||||
return CommandApdu(
|
||||
Instruction.Read, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
|
||||
return CardDeserializer.deserialize(apdu, environment)
|
||||
return CardDeserializer.deserialize(apdu)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,15 +103,11 @@ class ReadIssuerDataCommand(
|
|||
tlvBuilder.append(TlvTag.Pin, environment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
|
||||
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
|
||||
return CommandApdu(
|
||||
Instruction.ReadIssuerData, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerDataResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return ReadIssuerDataResponse(
|
||||
|
|
|
|||
|
|
@ -152,16 +152,11 @@ class ReadIssuerExtraDataCommand(
|
|||
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
|
||||
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
|
||||
tlvBuilder.append(TlvTag.Offset, offset)
|
||||
return CommandApdu(
|
||||
Instruction.ReadIssuerData, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerExtraDataResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return ReadIssuerExtraDataResponse(
|
||||
|
|
|
|||
|
|
@ -68,15 +68,11 @@ class ReadUserDataCommand : Command<ReadUserDataResponse>() {
|
|||
builder.append(TlvTag.CardId, environment.card?.cardId)
|
||||
builder.append(TlvTag.Pin, environment.pin1)
|
||||
|
||||
return CommandApdu(
|
||||
Instruction.ReadUserData, builder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.ReadUserData, builder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadUserDataResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return ReadUserDataResponse(
|
||||
|
|
|
|||
|
|
@ -99,10 +99,7 @@ class SignCommand(private val hashes: Array<ByteArray>)
|
|||
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
|
||||
|
||||
addTerminalSignature(environment, dataToSign, tlvBuilder)
|
||||
return CommandApdu(
|
||||
Instruction.Sign, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.Sign, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
private fun flattenHashes(): ByteArray {
|
||||
|
|
@ -133,8 +130,7 @@ class SignCommand(private val hashes: Array<ByteArray>)
|
|||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return SignResponse(
|
||||
|
|
|
|||
|
|
@ -114,15 +114,11 @@ class WriteIssuerDataCommand(
|
|||
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
|
||||
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
|
||||
|
||||
return CommandApdu(
|
||||
Instruction.WriteIssuerData, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
return WriteIssuerDataResponse(
|
||||
|
|
|
|||
|
|
@ -184,10 +184,7 @@ class WriteIssuerExtraDataCommand(
|
|||
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
|
||||
}
|
||||
}
|
||||
return CommandApdu(
|
||||
Instruction.WriteIssuerData, tlvBuilder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
private fun getDataToWrite(): ByteArray =
|
||||
|
|
@ -199,8 +196,7 @@ class WriteIssuerExtraDataCommand(
|
|||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -77,15 +77,12 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
|
|||
if (userProtectedCounter != null || userProtectedData != null)
|
||||
builder.append(TlvTag.Pin2, environment.pin2)
|
||||
|
||||
return CommandApdu(
|
||||
Instruction.WriteUserData, builder.serialize(),
|
||||
environment.encryptionMode, environment.encryptionKey
|
||||
)
|
||||
return CommandApdu(Instruction.WriteUserData, builder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteUserDataResponse {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.commands.common
|
||||
|
||||
import com.tangem.SessionEnvironment
|
||||
import com.tangem.TangemSdkError
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CardData
|
||||
|
|
@ -11,9 +10,8 @@ import com.tangem.common.tlv.TlvTag
|
|||
|
||||
class CardDeserializer() {
|
||||
companion object {
|
||||
fun deserialize(apdu: ResponseApdu, environment: SessionEnvironment): Card {
|
||||
val tlvData = apdu.getTlvData(environment.encryptionKey)
|
||||
?: throw TangemSdkError.DeserializeApduFailed()
|
||||
fun deserialize(apdu: ResponseApdu): Card {
|
||||
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
|
||||
|
||||
val decoder = TlvDecoder(tlvData)
|
||||
|
||||
|
|
|
|||
|
|
@ -47,15 +47,11 @@ class PersonalizeCommand(
|
|||
}
|
||||
|
||||
override fun serialize(environment: SessionEnvironment): CommandApdu {
|
||||
return CommandApdu(
|
||||
Instruction.Personalize,
|
||||
serializePersonalizationData(config),
|
||||
encryptionKey = devPersonalizationKey
|
||||
)
|
||||
return CommandApdu(Instruction.Personalize, serializePersonalizationData(config))
|
||||
}
|
||||
|
||||
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
|
||||
return CardDeserializer.deserialize(apdu, environment)
|
||||
return CardDeserializer.deserialize(apdu)
|
||||
}
|
||||
|
||||
private fun serializePersonalizationData(config: CardConfig): ByteArray {
|
||||
|
|
|
|||
|
|
@ -15,42 +15,27 @@ import java.io.ByteArrayOutputStream
|
|||
*/
|
||||
class CommandApdu(
|
||||
|
||||
private val ins: Int,
|
||||
private val tlvs: ByteArray,
|
||||
val ins: Int,
|
||||
private val tlvs: ByteArray,
|
||||
|
||||
private val le: Int = 0x00,
|
||||
private val p1: Int,
|
||||
private val p2: Int,
|
||||
|
||||
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||
private val encryptionKey: ByteArray? = null,
|
||||
private val le: Int = 0x00,
|
||||
|
||||
private val cla: Int = ISO_CLA) {
|
||||
private val cla: Int = ISO_CLA
|
||||
) {
|
||||
|
||||
constructor(
|
||||
instruction: Instruction,
|
||||
tlvs: ByteArray,
|
||||
encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||
encryptionKey: ByteArray? = null
|
||||
instruction: Instruction,
|
||||
tlvs: ByteArray
|
||||
) : this(
|
||||
instruction.code,
|
||||
tlvs,
|
||||
encryptionMode = encryptionMode,
|
||||
encryptionKey = encryptionKey
|
||||
instruction.code,
|
||||
tlvs,
|
||||
0,
|
||||
0
|
||||
)
|
||||
|
||||
private val p1: Int
|
||||
private val p2: Int
|
||||
|
||||
init {
|
||||
if (ins == Instruction.OpenSession.code) {
|
||||
p1 = 0x00
|
||||
p2 = encryptionMode.code.toInt()
|
||||
} else {
|
||||
p1 = encryptionMode.code.toInt()
|
||||
p2 = 0x00
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request converted to a raw data
|
||||
*/
|
||||
|
|
@ -62,7 +47,7 @@ class CommandApdu(
|
|||
|
||||
|
||||
private fun toBytes(): ByteArray {
|
||||
val data = if (encryptionKey != null) tlvs.encrypt() else tlvs
|
||||
val data = tlvs
|
||||
|
||||
val byteStream = ByteArrayOutputStream()
|
||||
byteStream.write(cla)
|
||||
|
|
@ -83,14 +68,21 @@ class CommandApdu(
|
|||
}
|
||||
|
||||
|
||||
private fun ByteArray.encrypt(): ByteArray {
|
||||
val crc: ByteArray = tlvs.calculateCrc16()
|
||||
val stream = ByteArrayOutputStream()
|
||||
stream.write(this.size.toByteArray(2))
|
||||
stream.write(crc)
|
||||
stream.write(this)
|
||||
return stream.toByteArray().encrypt(encryptionKey!!)
|
||||
fun encrypt(
|
||||
encryptionMode: EncryptionMode,
|
||||
encryptionKey: ByteArray?
|
||||
): CommandApdu {
|
||||
if (encryptionMode == EncryptionMode.NONE
|
||||
|| encryptionKey == null || p1 != EncryptionMode.NONE.code
|
||||
) {
|
||||
return this
|
||||
}
|
||||
val crc: ByteArray = tlvs.calculateCrc16()
|
||||
val dataToEncrypt = tlvs.size.toByteArray(2) + crc + tlvs
|
||||
val encryptedData = dataToEncrypt.encrypt(encryptionKey)
|
||||
|
||||
return CommandApdu(ins, encryptedData, encryptionMode.code, p2, le, cla)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ISO_CLA = 0x00
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.common.apdu
|
||||
|
||||
import com.tangem.TangemSdkError
|
||||
import com.tangem.common.extensions.calculateCrc16
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.crypto.decrypt
|
||||
|
|
@ -27,40 +28,36 @@ class ResponseApdu(private val data: ByteArray) {
|
|||
* @param encryptionKey key to decrypt response.
|
||||
* (Encryption / decryption functionality is not implemented yet.)
|
||||
*/
|
||||
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
|
||||
fun getTlvData(): List<Tlv>? {
|
||||
return if (data.size <= 2) {
|
||||
null
|
||||
} else {
|
||||
val responseData = data.copyOf(data.size - 2)
|
||||
return if (encryptionKey != null) {
|
||||
if (data.size >= 18) {
|
||||
val decryptedData = decrypt(responseData, encryptionKey)
|
||||
Tlv.deserialize(decryptedData)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Tlv.deserialize(responseData)
|
||||
}
|
||||
Tlv.deserialize(data.copyOf(data.size - 2))
|
||||
}
|
||||
}
|
||||
|
||||
private fun decrypt(responseData: ByteArray, encryptionKey: ByteArray): ByteArray {
|
||||
fun decrypt(encryptionKey: ByteArray?): ResponseApdu {
|
||||
if (encryptionKey == null) return this
|
||||
|
||||
if (data.size < 18) throw TangemSdkError.InvalidResponse()
|
||||
|
||||
val responseData = data.copyOf(data.size - 2)
|
||||
|
||||
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
|
||||
|
||||
val inputStream = ByteArrayInputStream(decryptedData)
|
||||
val baLength = ByteArray(2)
|
||||
inputStream.read(baLength)
|
||||
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
|
||||
if (length > decryptedData.size - 4) throw Exception("Can't decrypt - data size invalid")
|
||||
if (length > decryptedData.size - 4) throw TangemSdkError.InvalidResponse()
|
||||
val baCRC = ByteArray(2)
|
||||
inputStream.read(baCRC)
|
||||
val answerData = ByteArray(length)
|
||||
inputStream.read(answerData)
|
||||
val crc: ByteArray = answerData.calculateCrc16()
|
||||
if (!baCRC.contentEquals(crc)) throw Exception("Can't decrypt - crc invalid")
|
||||
if (!baCRC.contentEquals(crc)) throw TangemSdkError.InvalidResponse()
|
||||
|
||||
return answerData
|
||||
return ResponseApdu(answerData + data[data.size - 2] + data[data.size - 1])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ internal class ScanTask : CardSessionRunnable<Card> {
|
|||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
try {
|
||||
val card = CardDeserializer.deserialize(result.data, session.environment)
|
||||
val card = CardDeserializer.deserialize(result.data)
|
||||
callback(CompletionResult.Success(card))
|
||||
} catch (error: TangemSdkError) {
|
||||
callback(CompletionResult.Failure(error))
|
||||
|
|
|
|||
0
tangem-sdk-android-config/.circleci/config.yml
Executable file → Normal file
0
tangem-sdk-android-config/.circleci/config.yml
Executable file → Normal file
|
|
@ -52,5 +52,6 @@ fun TangemSdkError.localizedDescription(): Int {
|
|||
is TangemSdkError.WrongCardNumber -> R.string.error_wrong_card_number
|
||||
is TangemSdkError.WrongCardType -> R.string.error_wrong_card_type
|
||||
is TangemSdkError.CardError -> R.string.error_card_error
|
||||
is TangemSdkError.InvalidResponse -> R.string.error_invalid_response
|
||||
}
|
||||
}
|
||||
|
|
@ -84,11 +84,11 @@ class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener {
|
|||
nfcAdapter = null
|
||||
}
|
||||
|
||||
fun enableReaderMode() {
|
||||
private fun enableReaderMode() {
|
||||
nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, Bundle())
|
||||
}
|
||||
|
||||
fun disableReaderMode() {
|
||||
private fun disableReaderMode() {
|
||||
nfcAdapter?.disableReaderMode(activity)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import com.tangem.common.extensions.toHexString
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
|
||||
data class NfcTag(val type: TagType, val isoDep: IsoDep?, val nfcV: NfcV? = null)
|
||||
|
|
@ -61,11 +63,16 @@ class NfcReader : CardReader {
|
|||
//TODO: send user cancelled if (cancelled)
|
||||
}
|
||||
|
||||
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
|
||||
val data = apdu.apduData
|
||||
override suspend fun transceiveApdu(apdu: CommandApdu): CompletionResult<ResponseApdu> =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
transceiveApdu(apdu) { result ->
|
||||
if (continuation.isActive) continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
|
||||
val rawResponse: ByteArray? = try {
|
||||
transcieveAndLog(data, callback)
|
||||
transcieveAndLog(apdu.apduData)
|
||||
} catch (exception: TagLostException) {
|
||||
callback.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
|
||||
nfcTag = null
|
||||
|
|
@ -82,7 +89,7 @@ class NfcReader : CardReader {
|
|||
rawResponse?.let { callback.invoke(CompletionResult.Success(ResponseApdu(it))) }
|
||||
}
|
||||
|
||||
private fun transcieveAndLog(data: ByteArray, callback: (response: CompletionResult<ResponseApdu>) -> Unit): ByteArray? {
|
||||
private fun transcieveAndLog(data: ByteArray): ByteArray? {
|
||||
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data.size}")
|
||||
Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data.toHexString()}")
|
||||
val rawResponse = nfcTag?.isoDep?.transceive(data)
|
||||
|
|
|
|||
|
|
@ -29,4 +29,5 @@
|
|||
<string name="error_wrong_card_number">You tapped a different card. Please match the in-app Card ID to the your physical Card ID to continue this process.</string>
|
||||
<string name="error_wrong_card_type">This card is configured for a different app. Please see the information on your card and download the related app.</string>
|
||||
<string name="error_card_error">Your card is missing essential data</string>
|
||||
<string name="error_invalid_response">Invalid Response</string>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue