diff --git a/tangem-core/build.gradle b/tangem-core/build.gradle index 220c949038..bd00611a6a 100644 --- a/tangem-core/build.gradle +++ b/tangem-core/build.gradle @@ -11,6 +11,8 @@ dependencies { // kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin" implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7" // crypto implementation "com.madgag.spongycastle:core:1.58.0.0" @@ -21,8 +23,8 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' // tests - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' - testImplementation "com.google.truth:truth:1.0" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2' + testImplementation "com.google.truth:truth:1.0.1" } sourceCompatibility = "8" diff --git a/tangem-core/src/main/java/com/tangem/CardReader.kt b/tangem-core/src/main/java/com/tangem/CardReader.kt index 2327e13fe6..deb1f33f14 100644 --- a/tangem-core/src/main/java/com/tangem/CardReader.kt +++ b/tangem-core/src/main/java/com/tangem/CardReader.kt @@ -3,6 +3,8 @@ package com.tangem import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.ResponseApdu +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BroadcastChannel /** * Allows interaction between the phone or any other terminal and Tangem card. @@ -11,6 +13,18 @@ import com.tangem.common.apdu.ResponseApdu */ interface CardReader { + val tag: BroadcastChannel + 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 + /** * Sends data to the card and receives the reply. * @@ -23,10 +37,17 @@ interface CardReader { /** * Signals to [CardReader] to become ready to transceive data. */ - fun openSession() + fun startSession() /** * Signals to [CardReader] that no further NFC transition is expected. */ - fun closeSession() + fun stopSession(cancelled: Boolean = false) + + fun readSlixTag(callback: (result: CompletionResult) -> Unit) + +} + +interface ReadingActiveListener { + var readingIsActive: Boolean } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/CardSession.kt b/tangem-core/src/main/java/com/tangem/CardSession.kt index c7fa4559e7..8047252521 100644 --- a/tangem-core/src/main/java/com/tangem/CardSession.kt +++ b/tangem-core/src/main/java/com/tangem/CardSession.kt @@ -1,6 +1,5 @@ package com.tangem -import com.tangem.commands.Card import com.tangem.commands.CommandResponse import com.tangem.commands.OpenSessionCommand import com.tangem.commands.ReadCommand @@ -10,9 +9,9 @@ import com.tangem.common.apdu.ResponseApdu import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.getType import com.tangem.crypto.EncryptionHelper -import com.tangem.crypto.FastEncryptionHelper -import com.tangem.crypto.StrongEncryptionHelper import com.tangem.crypto.pbkdf2Hash +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* /** * Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession] @@ -30,6 +29,16 @@ interface CardSessionRunnable { fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) } +enum class CardSessionState { + Inactive, + Active +} + +enum class TagType { + Nfc, + Slix +} + /** * Allows interaction with Tangem cards. Should be opened before sending commands. * @@ -44,20 +53,25 @@ interface CardSessionRunnable { * 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 ) { - private val tag = this.javaClass.simpleName + var connectedTag: TagType? = null + /** * True if some operation is still in progress. */ - private var isBusy = false + private var state = CardSessionState.Inactive - private var performPreflightRead = true + val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { _, ex -> + throw ex + } + + private val tag = this.javaClass.simpleName /** * This metod starts a card session, performs preflight [ReadCommand], @@ -66,19 +80,14 @@ class CardSession( * @param callback will be triggered with a [CompletionResult] of a session. */ fun , R : CommandResponse> startWithRunnable( - runnable: T, callback: (result: CompletionResult) -> Unit) { + runnable: T, callback: (result: CompletionResult) -> Unit + ) { - performPreflightRead = runnable.performPreflightRead - - start { session, error -> + 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) { @@ -103,72 +112,77 @@ 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(callback: (session: CardSession, error: TangemSdkError?) -> Unit) { - try { - startSession() - } catch (error: TangemSdkError) { - callback(this, error) - } + fun start( + performPreflightRead: Boolean = true, + callback: (session: CardSession, error: TangemSdkError?) -> Unit + ) { - if (!performPreflightRead) { - callback(this, null) + if (state != CardSessionState.Inactive) { + callback(this, TangemSdkError.Busy()) return } + state = CardSessionState.Active + viewDelegate.onSessionStarted(cardId) - preflightRead() { result -> + scope.launch { + reader.tag + .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) + } + } + } + } + reader.scope = scope + reader.startSession() + } + + private fun handleTagLost() { + connectedTag = null + environment.encryptionKey = null + viewDelegate.onTagLost() + } + + private fun preflightCheck(callback: (session: CardSession, error: TangemSdkError?) -> Unit) { + val readCommand = ReadCommand() + readCommand.run(this) { result -> when (result) { is CompletionResult.Failure -> { - callback(this, result.error) stopWithError(result.error) + callback(this, result.error) } is CompletionResult.Success -> { + val receivedCardId = result.data.cardId + if (cardId != null && receivedCardId != cardId) { + viewDelegate.onWrongCard() + preflightCheck(callback) + return@run + } + val allowedCardTypes = environment.cardFilter.allowedCardTypes + if (!allowedCardTypes.contains(result.data.getType())) { + stopWithError(TangemSdkError.WrongCardType()) + callback(this, TangemSdkError.WrongCardType()) + return@run + } + environment.card = result.data + cardId = receivedCardId callback(this, null) } } } } - private fun startSession() { - if (isBusy) throw TangemSdkError.Busy() - isBusy = true - viewDelegate.onNfcSessionStarted(cardId, initialMessage) - reader.openSession() - } - - private fun preflightRead(callback: (result: CompletionResult) -> Unit) { - val readCommand = ReadCommand() - readCommand.run(this) { result -> - when (result) { - is CompletionResult.Failure -> { - tryHandleError(result.error) { handleErrorResult -> - when (handleErrorResult) { - is CompletionResult.Success -> preflightRead(callback) - is CompletionResult.Failure -> { - stopWithError(result.error) - callback(CompletionResult.Failure(result.error)) - } - } - } - } - is CompletionResult.Success -> { - val receivedCardId = result.data.cardId - if (cardId != null && receivedCardId != cardId) { - stopWithError(TangemSdkError.WrongCardNumber()) - callback(CompletionResult.Failure(TangemSdkError.WrongCardNumber())) - return@run - } - val allowedCardTypes = environment.cardFilter.allowedCardTypes - if (!allowedCardTypes.contains(result.data.getType())) { - stopWithError(TangemSdkError.WrongCardType()) - callback(CompletionResult.Failure(TangemSdkError.WrongCardType())) - return@run - } - environment.card = result.data - cardId = receivedCardId - callback(CompletionResult.Success(result.data)) - } - } - } + fun readSlixTag(callback: (result: CompletionResult) -> Unit) { + reader.readSlixTag(callback) } /** @@ -176,9 +190,8 @@ class CardSession( * @param message If null, the default message will be shown. */ private fun stop(message: Message? = null) { - reader.closeSession() - viewDelegate.onNfcSessionCompleted(message) - isBusy = false + stopSession() + viewDelegate.onSessionStopped(message) } /** @@ -186,75 +199,80 @@ class CardSession( * @param error An error that will be shown. */ private fun stopWithError(error: TangemSdkError) { - if (!isBusy) return - - reader.closeSession() - isBusy = false - - val errorMessage = if (error is TangemSdkError) { - "${error::class.simpleName}: ${error.code}" - } else { - error.localizedMessage - } + stopSession() if (error !is TangemSdkError.UserCancelled) { - Log.e(tag, "Finishing with error: $errorMessage") + Log.e(tag, "Finishing with error: ${error::class.simpleName}: ${error.code}") viewDelegate.onError(error) } else { Log.i(tag, "User cancelled NFC session") } + } + private fun stopSession() { + reader.stopSession() + state = CardSessionState.Inactive + scope.cancel() } fun send(apdu: CommandApdu, callback: (result: CompletionResult) -> Unit) { - reader.transceiveApdu(apdu, callback) - } - - private fun tryHandleError( - error: TangemSdkError, callback: (result: CompletionResult) -> 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())) - } + val subscription = reader.tag.openSubscription() + scope.launch { + subscription.consumeAsFlow() + .filterNotNull() + .map { establishEncryptionIfNeeded() } + .map { apdu.encrypt(environment.encryptionMode, environment.encryptionKey) } + .map { encryptedApdu -> reader.transceiveApdu(encryptedApdu) } + .map { responseApdu -> decrypt(responseApdu) } + .catch { if (it is TangemSdkError) callback(CompletionResult.Failure(it)) } + .collect { result -> + subscription.cancel() + callback(result) } - return establishEncryption(callback) - } - else -> callback(CompletionResult.Failure(TangemSdkError.UnknownError())) } } - private fun establishEncryption(callback: (result: CompletionResult) -> Unit) { - val encryptionHelper: EncryptionHelper = - if (environment.encryptionMode == EncryptionMode.STRONG) { - StrongEncryptionHelper() - } else { - FastEncryptionHelper() - } + private suspend fun establishEncryptionIfNeeded(): CompletionResult { + if (environment.encryptionMode == EncryptionMode.NONE || environment.encryptionKey != null) { + return CompletionResult.Success(true) + } + + val encryptionHelper = EncryptionHelper.create(environment.encryptionMode) + ?: return CompletionResult.Success(true) + 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)) + 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) } - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.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) + } + } + + private fun decrypt(result: CompletionResult): CompletionResult { + return when (result) { + is CompletionResult.Success -> { + try { + CompletionResult.Success( + result.data.decrypt(environment.encryptionKey) + ) + } catch (error: TangemSdkError) { + return CompletionResult.Failure(error) + } + } + is CompletionResult.Failure -> result } } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt b/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt index 5f8734f685..78b6d28715 100644 --- a/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt +++ b/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt @@ -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) diff --git a/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt b/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt index 969b147b8f..71311dcf00 100644 --- a/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt +++ b/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt @@ -12,7 +12,7 @@ interface SessionViewDelegate { /** * It is called when user is expected to scan a Tangem Card with an Android device. */ - fun onNfcSessionStarted(cardId: String?, message: Message? = null) + fun onSessionStarted(cardId: String?, message: Message? = null) /** * It is called when security delay is triggered by the card. @@ -32,10 +32,14 @@ interface SessionViewDelegate { */ fun onTagLost() + fun onTagConnected() + + fun onWrongCard() + /** * It is called when NFC session was completed and a user can take the card away from the Android device. */ - fun onNfcSessionCompleted(message: Message? = null) + fun onSessionStopped(message: Message? = null) /** * It is called when some error occur during NFC session. diff --git a/tangem-core/src/main/java/com/tangem/TangemSdk.kt b/tangem-core/src/main/java/com/tangem/TangemSdk.kt index dc8f8dbe06..c5ac313d34 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdk.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdk.kt @@ -374,7 +374,7 @@ class TangemSdk( fun startSession(cardId: String? = null, initialMessage: Message? = null, callback: (session: CardSession, error: TangemSdkError?) -> Unit) { val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage) - Thread().run { cardSession.start(callback) } + Thread().run { cardSession.start(callback = callback) } } /** diff --git a/tangem-core/src/main/java/com/tangem/TangemSdkError.kt b/tangem-core/src/main/java/com/tangem/TangemSdkError.kt index 4aabb3da6b..d58771fc77 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdkError.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdkError.kt @@ -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. @@ -104,7 +105,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { class TooManyHashesInOneTransaction : TangemSdkError(40906) //Write Extra Issuer Data Errors - class ExendedDataSizeTooLarge : TangemSdkError(41101) + class ExtendedDataSizeTooLarge : TangemSdkError(41101) //General Errors class NotPersonalized() : TangemSdkError(40001) diff --git a/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt index e3cbdf970a..672ca2da62 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt @@ -70,19 +70,14 @@ class CheckWalletCommand( } } - override fun performPreCheck( - session: CardSession, - callback: (result: CompletionResult) -> Unit - ): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.isActivated) { + return TangemSdkError.NotActivated() } - return false + return null } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -90,15 +85,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( diff --git a/tangem-core/src/main/java/com/tangem/commands/Command.kt b/tangem-core/src/main/java/com/tangem/commands/Command.kt index 0b17642735..3b25619ce6 100644 --- a/tangem-core/src/main/java/com/tangem/commands/Command.kt +++ b/tangem-core/src/main/java/com/tangem/commands/Command.kt @@ -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 { + /** + * 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,88 +35,105 @@ interface CommandResponse /** * Basic class for Tangem card commands */ -abstract class Command : CardSessionRunnable { +abstract class Command : ApduSerializable, CardSessionRunnable { 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) -> Unit) { Log.i("Command", "Initializing ${this::class.java.simpleName}") - if (session.environment.handleErrors) { - if (performPreCheck(session, callback)) return - } - transceive(session) { result -> - if (session.environment.handleErrors) { - if (performAfterCheck(session, result, callback)) return@transceive - } - callback(result) - } + transceive(session, callback) } - open fun performPreCheck(session: CardSession, - callback: (result: CompletionResult) -> Unit): Boolean { - return false - } + open fun performPreCheck(card: Card): TangemSdkError? = null - open fun performAfterCheck(session: CardSession, - result: CompletionResult, - callback: (result: CompletionResult) -> Unit): Boolean { - return false - } + open fun mapError(card: Card?, error: TangemSdkError): TangemSdkError = error fun transceive(session: CardSession, callback: (result: CompletionResult) -> Unit) { - try { - val apdu = serialize(session.environment) - transceiveApdu(apdu, session) { result -> - when (result) { - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) - is CompletionResult.Success -> { + + val card = session.environment.card + if (session.environment.handleErrors && card != null) { + performPreCheck(card)?.let { error -> + callback(CompletionResult.Failure(error)) + return + } + } + + val apdu = serialize(session.environment) + transceiveApdu(apdu, session) { result -> + when (result) { + is CompletionResult.Failure -> { + if (session.environment.handleErrors) { + val error = mapError(session.environment.card, result.error) + callback(CompletionResult.Failure(error)) + return@transceiveApdu + + } + callback(CompletionResult.Failure(result.error)) + } + is CompletionResult.Success -> { + try { val response = deserialize(session.environment, result.data) callback(CompletionResult.Success(response)) + } catch (error: TangemSdkError) { + callback(CompletionResult.Failure(error)) } } } - } catch (error: TangemSdkError) { - callback(CompletionResult.Failure(error)) } + } - private fun transceiveApdu(apdu: CommandApdu, session: CardSession, callback: (result: CompletionResult) -> Unit) { - session.send(apdu) { result -> + private fun transceiveApdu( + apdu: CommandApdu, + session: CardSession, + callback: (result: CompletionResult) -> 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 -> { + callback(CompletionResult.Success(responseApdu)) + } 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 +161,9 @@ abstract class Command : CardSessionRunnable { * * @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() } diff --git a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt index e0bf433c99..87f9f6dcbe 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt @@ -1,9 +1,7 @@ package com.tangem.commands -import com.tangem.CardSession import com.tangem.SessionEnvironment import com.tangem.TangemSdkError -import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -12,18 +10,18 @@ import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag class CreateWalletResponse( - /** - * CID, Unique Tangem card ID number. - */ - val cardId: String, - /** - * Current status of the card [1 - Empty, 2 - Loaded, 3- Purged] - */ - val status: CardStatus, - /** + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, + /** + * Current status of the card [1 - Empty, 2 - Loaded, 3- Purged] + */ + val status: CardStatus, + /** - */ - val walletPublicKey: ByteArray + */ + val walletPublicKey: ByteArray ) : CommandResponse /** @@ -39,39 +37,25 @@ class CreateWalletResponse( */ class CreateWalletCommand : Command() { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.isActivated) { + return TangemSdkError.NotActivated() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + + return when (card.status) { + CardStatus.Empty -> null + CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized() + CardStatus.Loaded -> TangemSdkError.AlreadyCreated() + CardStatus.Purged -> TangemSdkError.CardIsPurged() + null -> TangemSdkError.CardError() } - if (session.environment.card?.status == CardStatus.Purged) { - callback(CompletionResult.Failure(TangemSdkError.CardIsPurged())) - return true - } - if (session.environment.card?.status == CardStatus.Loaded) { - callback(CompletionResult.Failure(TangemSdkError.AlreadyCreated())) - return true - } - return false } - override fun performAfterCheck(session: CardSession, - result: CompletionResult, - callback: (result: CompletionResult) -> Unit): Boolean { - when (result) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.InvalidParams) { - callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired())) - return true - } - return false - } - else -> return false + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams) { + return TangemSdkError.Pin2OrCvcRequired() } + return error } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -80,21 +64,21 @@ class CreateWalletCommand : Command() { 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) - ?: throw TangemSdkError.DeserializeApduFailed() + override fun deserialize( + environment: SessionEnvironment, + apdu: ResponseApdu + ): CreateWalletResponse { + val tlvData = apdu.getTlvData() + ?: throw TangemSdkError.DeserializeApduFailed() val decoder = TlvDecoder(tlvData) return CreateWalletResponse( - cardId = decoder.decode(TlvTag.CardId), - status = decoder.decode(TlvTag.Status), - walletPublicKey = decoder.decode(TlvTag.WalletPublicKey) + cardId = decoder.decode(TlvTag.CardId), + status = decoder.decode(TlvTag.Status), + walletPublicKey = decoder.decode(TlvTag.WalletPublicKey) ) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt b/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt index 891e28f3c1..1301703b9f 100644 --- a/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt @@ -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() { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.isActivated) { + return TangemSdkError.NotActivated() } - if (session.environment.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) { - callback(CompletionResult.Failure(TangemSdkError.PurgeWalletProhibited())) - return true + if (card.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) { + return TangemSdkError.PurgeWalletProhibited() + } + + return when (card.status) { + CardStatus.Loaded -> null + CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized() + CardStatus.Empty, CardStatus.Purged -> TangemSdkError.CardIsEmpty() + null -> TangemSdkError.CardError() } - return false } - override fun performAfterCheck(session: CardSession, - result: CompletionResult, - callback: (result: CompletionResult) -> Unit): Boolean { - when (result) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.InvalidParams) { - callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired())) - return true - } - return false - } - else -> return false + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams) { + return TangemSdkError.Pin2OrCvcRequired() } + return error } + override fun serialize(environment: SessionEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() 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( diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt index c8bbda44ff..189588a080 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt @@ -1,15 +1,12 @@ package com.tangem.commands -import com.tangem.CardSession import com.tangem.SessionEnvironment import com.tangem.TangemSdkError -import com.tangem.common.CompletionResult +import com.tangem.commands.common.CardDeserializer import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu -import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag import java.util.* @@ -318,6 +315,8 @@ class Card( /** * Whether the card requires issuer’s confirmation of activation. + * is "true" if the card requires activation, + * is 'false" if the card is activated or does not require activation */ val isActivated: Boolean, @@ -368,17 +367,11 @@ class Card( */ class ReadCommand : Command() { - override fun performAfterCheck(session: CardSession, result: CompletionResult, callback: (result: CompletionResult) -> Unit): Boolean { - when (result) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.InvalidParams) { - callback(CompletionResult.Failure(TangemSdkError.Pin1Required())) - return true - } - return false - } - else -> return false + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams) { + return TangemSdkError.Pin1Required() } + return error } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -391,64 +384,10 @@ class ReadCommand : Command() { */ 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 { - val tlvData = apdu.getTlvData(environment.encryptionKey) - ?: throw TangemSdkError.DeserializeApduFailed() - - val decoder = TlvDecoder(tlvData) - - return Card( - cardId = decoder.decodeOptional(TlvTag.CardId) ?: "", - manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "", - status = decoder.decodeOptional(TlvTag.Status), - - firmwareVersion = decoder.decodeOptional(TlvTag.Firmware), - cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey), - settingsMask = decoder.decodeOptional(TlvTag.SettingsMask), - issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey), - curve = decoder.decodeOptional(TlvTag.CurveId), - maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures), - signingMethods = decoder.decodeOptional(TlvTag.SigningMethod), - pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2), - walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey), - walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures), - walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes), - health = decoder.decodeOptional(TlvTag.Health), - isActivated = decoder.decode(TlvTag.IsActivated), - activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed), - paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion), - userCounter = decoder.decodeOptional(TlvTag.UserCounter), - userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter), - terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked), - - cardData = deserializeCardData(tlvData) - ) - } - - private fun deserializeCardData(tlvData: List): CardData? { - val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let { - Tlv.deserialize(it.value) - } - if (cardDataTlvs.isNullOrEmpty()) return null - - val decoder = TlvDecoder(cardDataTlvs) - return CardData( - batchId = decoder.decodeOptional(TlvTag.Batch), - manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime), - issuerName = decoder.decodeOptional(TlvTag.IssuerId), - blockchainName = decoder.decodeOptional(TlvTag.BlockchainId), - manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature), - productMask = decoder.decodeOptional(TlvTag.ProductMask), - - tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol), - tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress), - tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal) - ) + return CardDeserializer.deserialize(apdu) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt index d01a3adbe8..97bc306083 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt @@ -17,32 +17,32 @@ import com.tangem.common.tlv.TlvTag class ReadIssuerDataResponse( - /** - * CID, Unique Tangem card ID number. - */ - val cardId: String, + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, - /** - * Data defined by issuer. - */ - val issuerData: ByteArray, + /** + * Data defined by issuer. + */ + val issuerData: ByteArray, - /** - * Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card). - * Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]: - * SHA256([cardId] | [issuerData]). - * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of - * SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]: - * SHA256([cardId] | [issuerData] | [issuerDataCounter]). - */ - val issuerDataSignature: ByteArray, + /** + * Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card). + * Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]: + * SHA256([cardId] | [issuerData]). + * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of + * SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]: + * SHA256([cardId] | [issuerData] | [issuerDataCounter]). + */ + val issuerDataSignature: ByteArray, - /** - * An optional counter that protect issuer data against replay attack. - * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] - * then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand]. - */ - val issuerDataCounter: Int? + /** + * An optional counter that protect issuer data against replay attack. + * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] + * then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand]. + */ + val issuerDataCounter: Int? ) : CommandResponse @@ -54,21 +54,17 @@ class ReadIssuerDataResponse( * @property cardId CID, Unique Tangem card ID number. */ class ReadIssuerDataCommand( - val issuerPublicKey: ByteArray? = null, - verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() + val issuerPublicKey: ByteArray? = null, + verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() ) : Command(), IssuerDataVerifier by verifier { - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val card = session.environment.card - if (card == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) - return - } - val publicKey = issuerPublicKey ?: card.issuerPublicKey - if (publicKey == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey())) - return - } + override fun run( + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + + val publicKey = issuerPublicKey ?: session.environment.card?.issuerPublicKey + super.run(session) { result -> when (result) { is CompletionResult.Failure -> callback(result) @@ -78,9 +74,9 @@ class ReadIssuerDataCommand( return@run } val issuerDataToVerify = IssuerDataToVerify( - card.cardId, result.data.issuerData, result.data.issuerDataCounter + result.data.cardId, result.data.issuerData, result.data.issuerDataCounter ) - if (verify(publicKey, result.data.issuerDataSignature, issuerDataToVerify)) { + if (verify(publicKey!!, result.data.issuerDataSignature, issuerDataToVerify)) { callback(result) } else { callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) @@ -90,12 +86,12 @@ class ReadIssuerDataCommand( } } - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - return false + issuerPublicKey ?: card.issuerPublicKey ?: return TangemSdkError.MissingIssuerPubicKey() + return null } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -103,22 +99,21 @@ 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() + override fun deserialize( + environment: SessionEnvironment, + apdu: ResponseApdu + ): ReadIssuerDataResponse { + val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed() val decoder = TlvDecoder(tlvData) return ReadIssuerDataResponse( - cardId = decoder.decode(TlvTag.CardId), - issuerData = decoder.decode(TlvTag.IssuerData), - issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature), - issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter) + cardId = decoder.decode(TlvTag.CardId), + issuerData = decoder.decode(TlvTag.IssuerData), + issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature), + issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter) ) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt index 82e148a05d..cb7bba09d0 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt @@ -18,37 +18,37 @@ import java.io.ByteArrayOutputStream class ReadIssuerExtraDataResponse( - /** - * CID, Unique Tangem card ID number. - */ - val cardId: String, + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, - /** - * Size of all Issuer_Extra_Data field. - */ - val size: Int?, + /** + * Size of all Issuer_Extra_Data field. + */ + val size: Int?, - /** - * Data defined by issuer. - */ - val issuerData: ByteArray, + /** + * Data defined by issuer. + */ + val issuerData: ByteArray, - /** - * Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card). - * Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]: - * SHA256([cardId] | [issuerData]). - * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of - * SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]: - * SHA256([cardId] | [issuerData] | [issuerDataCounter]). - */ - val issuerDataSignature: ByteArray?, + /** + * Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card). + * Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]: + * SHA256([cardId] | [issuerData]). + * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of + * SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]: + * SHA256([cardId] | [issuerData] | [issuerDataCounter]). + */ + val issuerDataSignature: ByteArray?, - /** - * An optional counter that protects issuer data against replay attack. - * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] - * then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand]. - */ - val issuerDataCounter: Int? + /** + * An optional counter that protects issuer data against replay attack. + * When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] + * then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand]. + */ + val issuerDataCounter: Int? ) : CommandResponse @@ -60,42 +60,38 @@ class ReadIssuerExtraDataResponse( * a series of these commands have to be executed to read the entire Issuer_Extra_Data. */ class ReadIssuerExtraDataCommand( - private val issuerPublicKey: ByteArray? = null, - verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() + private val issuerPublicKey: ByteArray? = null, + verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() ) : Command(), IssuerDataVerifier by verifier { private val issuerData = ByteArrayOutputStream() private var offset: Int = 0 private var issuerDataSize: Int = 0 - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val card = session.environment.card - if (card == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) - return + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - val publicKey = issuerPublicKey ?: card.issuerPublicKey - if (publicKey == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey())) - return - } - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return - } - - readIssuerData(session, card.cardId, publicKey, callback) + issuerPublicKey ?: card.issuerPublicKey ?: return TangemSdkError.MissingIssuerPubicKey() + return null } + override fun run( + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + val publicKey = issuerPublicKey ?: session.environment.card?.issuerPublicKey + readIssuerData(session, publicKey!!, callback) + } private fun readIssuerData( - session: CardSession, - cardId: String, publicKey: ByteArray, - callback: (result: CompletionResult) -> Unit) { + session: CardSession, publicKey: ByteArray, + callback: (result: CompletionResult) -> Unit + ) { if (issuerDataSize != 0) { session.viewDelegate.onDelay( - issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE + issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE ) } @@ -112,9 +108,9 @@ class ReadIssuerExtraDataCommand( issuerData.write(result.data.issuerData) if (result.data.issuerDataSignature == null) { offset = issuerData.size() - readIssuerData(session, cardId, publicKey, callback) + readIssuerData(session, publicKey, callback) } else { - completeTask(result.data, cardId, publicKey, callback) + completeTask(result.data, publicKey, callback) } } is CompletionResult.Failure -> { @@ -124,21 +120,22 @@ class ReadIssuerExtraDataCommand( } } - private fun completeTask(data: ReadIssuerExtraDataResponse, - cardId: String, publicKey: ByteArray, - callback: (result: CompletionResult) -> Unit) { + private fun completeTask( + data: ReadIssuerExtraDataResponse, publicKey: ByteArray, + callback: (result: CompletionResult) -> Unit + ) { val dataToVerify = IssuerDataToVerify( - cardId, - issuerData.toByteArray(), - data.issuerDataCounter + data.cardId, + issuerData.toByteArray(), + data.issuerDataCounter ) if (verify(publicKey, data.issuerDataSignature!!, dataToVerify)) { val finalResult = ReadIssuerExtraDataResponse( - data.cardId, - issuerDataSize, - issuerData.toByteArray(), - data.issuerDataSignature, - data.issuerDataCounter + data.cardId, + issuerDataSize, + issuerData.toByteArray(), + data.issuerDataSignature, + data.issuerDataCounter ) callback(CompletionResult.Success(finalResult)) } else { @@ -152,24 +149,22 @@ 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() - + override fun deserialize( + environment: SessionEnvironment, + apdu: ResponseApdu + ): ReadIssuerExtraDataResponse { + val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed() val decoder = TlvDecoder(tlvData) return ReadIssuerExtraDataResponse( - cardId = decoder.decode(TlvTag.CardId), - size = decoder.decodeOptional(TlvTag.Size), - issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(), - issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature), - issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter) + cardId = decoder.decode(TlvTag.CardId), + size = decoder.decodeOptional(TlvTag.Size), + issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(), + issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature), + issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter) ) } diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt index b8ee2022d6..2e68b631ef 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt @@ -1,9 +1,7 @@ package com.tangem.commands -import com.tangem.CardSession import com.tangem.SessionEnvironment import com.tangem.TangemSdkError -import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -51,16 +49,14 @@ class ReadUserDataResponse( */ class ReadUserDataCommand : Command() { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.isActivated) { + return TangemSdkError.NotActivated() } - return false + return null } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -68,15 +64,11 @@ class ReadUserDataCommand : Command() { 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( diff --git a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt index fc926be116..c5d743975b 100644 --- a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt @@ -1,9 +1,7 @@ package com.tangem.commands -import com.tangem.CardSession import com.tangem.SessionEnvironment import com.tangem.TangemSdkError -import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -20,10 +18,10 @@ import com.tangem.crypto.sign * Sums up array elements within all SIGN commands */ class SignResponse( - val cardId: String, - val signature: ByteArray, - val walletRemainingSignatures: Int, - val walletSignedHashes: Int + val cardId: String, + val signature: ByteArray, + val walletRemainingSignatures: Int, + val walletSignedHashes: Int ) : CommandResponse /** @@ -32,60 +30,44 @@ class SignResponse( * @property hashes Array of transaction hashes. * @property cardId CID, Unique Tangem card ID number */ -class SignCommand(private val hashes: Array) - : Command() { +class SignCommand(private val hashes: Array) : Command() { + + //TODO: Allow signing more than 10 hashes private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0 - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.isActivated) { + return TangemSdkError.NotActivated() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.walletRemainingSignatures == 0) { + return TangemSdkError.NoRemainingSignatures() } - if (session.environment.card?.status == CardStatus.Purged) { - callback(CompletionResult.Failure(TangemSdkError.CardIsPurged())) - return true - } - if (session.environment.card?.status == CardStatus.Empty) { - callback(CompletionResult.Failure(TangemSdkError.CardIsEmpty())) - return true - } - if (session.environment.card?.walletRemainingSignatures == 0) { - callback(CompletionResult.Failure(TangemSdkError.NoRemainingSignatures())) - return true - } - if (session.environment.card?.signingMethods?.contains(SigningMethod.SignHash) != true) { - callback(CompletionResult.Failure(TangemSdkError.SignHashesNotAvailable())) - return true + if (card.signingMethods?.contains(SigningMethod.SignHash) != true) { + return TangemSdkError.SignHashesNotAvailable() } if (hashSizes == 0) { - callback(CompletionResult.Failure(TangemSdkError.EmptyHashes())) - return true + return TangemSdkError.EmptyHashes() } if (hashes.any { it.size != hashSizes }) { - callback(CompletionResult.Failure(TangemSdkError.HashSizeMustBeEqual())) - return true + return TangemSdkError.HashSizeMustBeEqual() + } + + return when (card.status) { + CardStatus.Loaded -> null + CardStatus.Empty -> TangemSdkError. + CardIsEmpty() + CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized() + CardStatus.Purged -> TangemSdkError.CardIsPurged() + null -> TangemSdkError.CardError() } - return false } - override fun performAfterCheck(session: CardSession, - result: CompletionResult, - callback: (result: CompletionResult) -> Unit): Boolean { - when (result) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.InvalidParams) { - callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired())) - return true - } - return false - } - else -> return false + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams) { + return TangemSdkError.Pin2OrCvcRequired() } + return error } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -99,23 +81,13 @@ class SignCommand(private val hashes: Array) 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 { - checkForErrors() return hashes.reduce { arr1, arr2 -> arr1 + arr2 } } - private fun checkForErrors() { - if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes() - if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction() - if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual() - } - /** * Application can optionally submit a public key Terminal_PublicKey in [SignCommand]. * Submitted key is stored by the Tangem card if it differs from a previous submitted Terminal_PublicKey. @@ -124,7 +96,8 @@ class SignCommand(private val hashes: Array) * (this key should be generated and securily stored by the application). */ private fun addTerminalSignature( - environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder) { + environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder + ) { environment.terminalKeys?.let { terminalKeyPair -> val signedData = dataToSign.sign(terminalKeyPair.privateKey) tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData) @@ -133,15 +106,14 @@ class SignCommand(private val hashes: Array) } 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( - cardId = decoder.decode(TlvTag.CardId), - signature = decoder.decode(TlvTag.Signature), - walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures), - walletSignedHashes = decoder.decode(TlvTag.SignedHashes) + cardId = decoder.decode(TlvTag.CardId), + signature = decoder.decode(TlvTag.Signature), + walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures), + walletSignedHashes = decoder.decode(TlvTag.SignedHashes) ) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt index 2574a2f4a6..0b563a018e 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt @@ -1,13 +1,11 @@ package com.tangem.commands -import com.tangem.CardSession import com.tangem.SessionEnvironment import com.tangem.TangemSdkError import com.tangem.commands.common.DefaultIssuerDataVerifier import com.tangem.commands.common.IssuerDataMode import com.tangem.commands.common.IssuerDataToVerify import com.tangem.commands.common.IssuerDataVerifier -import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -16,10 +14,10 @@ import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag class WriteIssuerDataResponse( - /** - * CID, Unique Tangem card ID number. - */ - val cardId: String + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String ) : CommandResponse /** @@ -33,75 +31,53 @@ class WriteIssuerDataResponse( * @property issuerDataCounter An optional counter that protect issuer data against replay attack. */ class WriteIssuerDataCommand( - private val issuerData: ByteArray, - private val issuerDataSignature: ByteArray, - private val issuerDataCounter: Int? = null, - private val issuerPublicKey: ByteArray? = null, - verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() + private val issuerData: ByteArray, + private val issuerDataSignature: ByteArray, + private val issuerDataCounter: Int? = null, + private val issuerPublicKey: ByteArray? = null, + verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() ) : Command(), IssuerDataVerifier by verifier { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - val card = session.environment.card - if (card == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) - return true - } + override fun performPreCheck(card: Card): TangemSdkError? { val publicKey = issuerPublicKey ?: card.issuerPublicKey - if (publicKey == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey())) - return true + ?: return TangemSdkError.MissingIssuerPubicKey() + + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true - } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.isActivated) { + return TangemSdkError.NotActivated() } if (issuerData.size > MAX_SIZE) { - callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge())) - return true + return TangemSdkError.DataSizeTooLarge() } if (!isCounterValid(issuerDataCounter, card)) { - callback(CompletionResult.Failure(TangemSdkError.MissingCounter())) - return true + return TangemSdkError.MissingCounter() } if (!verifySignature(publicKey, card.cardId)) { - callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) - return true + return TangemSdkError.VerificationFailed() } - return false + return null } - override fun performAfterCheck(session: CardSession, - result: CompletionResult, - callback: (result: CompletionResult) -> Unit - ): Boolean { - when (result) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.InvalidParams && - isCounterRequired(session.environment.card)) { - callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten())) - return true - } - return false - } - else -> return false + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams && isCounterRequired(card)) { + return TangemSdkError.DataCannotBeWritten() } + return error } private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean = - if (isCounterRequired(card)) issuerDataCounter != null else true + if (isCounterRequired(card)) issuerDataCounter != null else true private fun isCounterRequired(card: Card?): Boolean = - card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false + card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true private fun verifySignature(publicKey: ByteArray, cardId: String): Boolean { return verify( - publicKey, - issuerDataSignature, - IssuerDataToVerify(cardId, issuerData, issuerDataCounter) + publicKey, + issuerDataSignature, + IssuerDataToVerify(cardId, issuerData, issuerDataCounter) ) } @@ -114,19 +90,18 @@ 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() + override fun deserialize( + environment: SessionEnvironment, + apdu: ResponseApdu + ): WriteIssuerDataResponse { + val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed() val decoder = TlvDecoder(tlvData) return WriteIssuerDataResponse( - cardId = decoder.decode(TlvTag.CardId) + cardId = decoder.decode(TlvTag.CardId) ) } diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt index 84e0b50851..115ebcd1dd 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt @@ -32,89 +32,64 @@ import com.tangem.common.tlv.TlvTag * @param issuerDataCounter An optional counter that protect issuer data against replay attack. */ class WriteIssuerExtraDataCommand( - private val issuerData: ByteArray, - private val startingSignature: ByteArray, - private val finalizingSignature: ByteArray, - private val issuerDataCounter: Int? = null, - private val issuerPublicKey: ByteArray? = null, - verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() + private val issuerData: ByteArray, + private val startingSignature: ByteArray, + private val finalizingSignature: ByteArray, + private val issuerDataCounter: Int? = null, + private val issuerPublicKey: ByteArray? = null, + verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() ) : Command(), IssuerDataVerifier by verifier { var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData var offset: Int = 0 - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val card = session.environment.card - if (card == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) - return - } - val publicKey = issuerPublicKey ?: card.issuerPublicKey - if (publicKey == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey())) - return - } - - writeIssuerData(session, card.cardId, publicKey) { response -> - when (response) { - is CompletionResult.Success -> callback(response) - is CompletionResult.Failure -> { - if (response.error is TangemSdkError.InvalidParams && isCounterRequired(card)) { - callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten())) - return@writeIssuerData - } - if (response.error is TangemSdkError.InvalidState && - card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false) { - callback(CompletionResult.Failure(TangemSdkError.OverwritingDataIsProhibited())) - return@writeIssuerData - } - } - } - } + override fun run( + session: CardSession, + callback: (result: CompletionResult) -> Unit + ) { + writeIssuerData(session, callback) } - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - val card = session.environment.card - if (card == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) - return true - } + override fun performPreCheck(card: Card): TangemSdkError? { val publicKey = issuerPublicKey ?: card.issuerPublicKey - if (publicKey == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey())) - return true - } + ?: return TangemSdkError.MissingIssuerPubicKey() - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.isActivated) { + return TangemSdkError.NotActivated() } if (issuerData.size > MAX_SIZE) { - callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge())) - return true + return TangemSdkError.DataSizeTooLarge() } if (!isCounterValid(issuerDataCounter, card)) { - callback(CompletionResult.Failure(TangemSdkError.MissingCounter())) - return true + return TangemSdkError.MissingCounter() } - if (!verifySignatures(card.cardId, publicKey)) { - callback(CompletionResult.Failure(TangemSdkError.VerificationFailed())) - return true + if (!verifySignatures(publicKey, card.cardId)) { + return TangemSdkError.VerificationFailed() } - return false + return null + } + + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams && isCounterRequired(card)) { + return TangemSdkError.DataCannotBeWritten() + } + if (error is TangemSdkError.InvalidState && + card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true) { + return TangemSdkError.OverwritingDataIsProhibited() + } + return error } private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean = - if (isCounterRequired(card)) issuerDataCounter != null else true + if (isCounterRequired(card)) issuerDataCounter != null else true - private fun isCounterRequired(card: Card): Boolean = - card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false + private fun isCounterRequired(card: Card?): Boolean = + card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true - private fun verifySignatures(cardId: String, publicKey: ByteArray): Boolean { + private fun verifySignatures(publicKey: ByteArray, cardId: String): Boolean { val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size) val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter) @@ -124,13 +99,16 @@ class WriteIssuerExtraDataCommand( } private fun writeIssuerData( - session: CardSession, - cardId: String, publicKey: ByteArray, - callback: (result: CompletionResult) -> Unit + session: CardSession, + callback: (result: CompletionResult) -> Unit ) { if (mode == IssuerDataMode.WriteExtraData) { - session.viewDelegate.onDelay(issuerData.size, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE) + session.viewDelegate.onDelay( + issuerData.size, + offset, + SINGLE_WRITE_SIZE + ) } transceive(session) { result -> when (result) { @@ -138,7 +116,7 @@ class WriteIssuerExtraDataCommand( when (mode) { IssuerDataMode.InitializeWritingExtraData -> { mode = IssuerDataMode.WriteExtraData - writeIssuerData(session, cardId, publicKey, callback) + writeIssuerData(session, callback) return@transceive } IssuerDataMode.WriteExtraData -> { @@ -146,7 +124,7 @@ class WriteIssuerExtraDataCommand( if (offset >= issuerData.size) { mode = IssuerDataMode.FinalizeExtraData } - writeIssuerData(session, cardId, publicKey, callback) + writeIssuerData(session, callback) return@transceive } IssuerDataMode.FinalizeExtraData -> { @@ -155,6 +133,11 @@ class WriteIssuerExtraDataCommand( } } is CompletionResult.Failure -> { + if (session.environment.handleErrors) { + mapError(session.environment.card, result.error)?.let { + callback(CompletionResult.Failure(it)) + } + } callback(CompletionResult.Failure(result.error)) } } @@ -184,25 +167,25 @@ 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 = - issuerData.copyOfRange(offset, offset + calculatePartSize()) + issuerData.copyOfRange(offset, offset + calculatePartSize()) private fun calculatePartSize(): Int { val bytesLeft = issuerData.size - offset return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE } - override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse { - val tlvData = apdu.getTlvData(environment.encryptionKey) - ?: throw TangemSdkError.DeserializeApduFailed() + override fun deserialize( + environment: SessionEnvironment, + apdu: ResponseApdu + ): WriteIssuerDataResponse { + val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed() - return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId) + return WriteIssuerDataResponse( + cardId = TlvDecoder(tlvData).decode(TlvTag.CardId) ) } diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt index fc0d0467ae..97a406a2d3 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt @@ -1,9 +1,7 @@ package com.tangem.commands -import com.tangem.CardSession import com.tangem.SessionEnvironment import com.tangem.TangemSdkError -import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -34,36 +32,24 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val private val userCounter: Int? = null, private val userProtectedCounter: Int? = null) : Command() { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status == CardStatus.NotPersonalized) { + return TangemSdkError.NotPersonalized() } - if (session.environment.card?.isActivated == true) { - callback(CompletionResult.Failure(TangemSdkError.NotActivated())) - return true + if (card.isActivated) { + return TangemSdkError.NotActivated() } if (userData?.size ?: 0 > MAX_SIZE || userProtectedData?.size ?: 0 > MAX_SIZE) { - callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge())) - return true + return TangemSdkError.DataSizeTooLarge() } - return false + return null } - override fun performAfterCheck(session: CardSession, - result: CompletionResult, - callback: (result: CompletionResult) -> Unit - ): Boolean { - when (result) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.InvalidParams) { - callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired())) - return true - } - return false - } - else -> return false + override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError { + if (error is TangemSdkError.InvalidParams) { + return TangemSdkError.Pin2OrCvcRequired() } + return error } override fun serialize(environment: SessionEnvironment): CommandApdu { @@ -77,15 +63,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)) } diff --git a/tangem-core/src/main/java/com/tangem/commands/common/CardDeserializer.kt b/tangem-core/src/main/java/com/tangem/commands/common/CardDeserializer.kt new file mode 100644 index 0000000000..ccf809cf65 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/common/CardDeserializer.kt @@ -0,0 +1,67 @@ +package com.tangem.commands.common + +import com.tangem.TangemSdkError +import com.tangem.commands.Card +import com.tangem.commands.CardData +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.tlv.Tlv +import com.tangem.common.tlv.TlvDecoder +import com.tangem.common.tlv.TlvTag + +class CardDeserializer() { + companion object { + fun deserialize(apdu: ResponseApdu): Card { + val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed() + + val decoder = TlvDecoder(tlvData) + + return Card( + cardId = decoder.decodeOptional(TlvTag.CardId) ?: "", + manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "", + status = decoder.decodeOptional(TlvTag.Status), + + firmwareVersion = decoder.decodeOptional(TlvTag.Firmware), + cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey), + settingsMask = decoder.decodeOptional(TlvTag.SettingsMask), + issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey), + curve = decoder.decodeOptional(TlvTag.CurveId), + maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures), + signingMethods = decoder.decodeOptional(TlvTag.SigningMethod), + pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2), + walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey), + walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures), + walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes), + health = decoder.decodeOptional(TlvTag.Health), + isActivated = decoder.decode(TlvTag.IsActivated), + activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed), + paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion), + userCounter = decoder.decodeOptional(TlvTag.UserCounter), + userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter), + terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked), + + cardData = deserializeCardData(tlvData) + ) + } + + private fun deserializeCardData(tlvData: List): CardData? { + val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let { + Tlv.deserialize(it.value) + } + if (cardDataTlvs.isNullOrEmpty()) return null + + val decoder = TlvDecoder(cardDataTlvs) + return CardData( + batchId = decoder.decodeOptional(TlvTag.Batch), + manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime), + issuerName = decoder.decodeOptional(TlvTag.IssuerId), + blockchainName = decoder.decodeOptional(TlvTag.BlockchainId), + manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature), + productMask = decoder.decodeOptional(TlvTag.ProductMask), + + tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol), + tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress), + tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal) + ) + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt index 52d85278fa..643b374a13 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt @@ -1,12 +1,14 @@ package com.tangem.commands.personalization import com.tangem.CardSession +import com.tangem.EncryptionMode import com.tangem.SessionEnvironment import com.tangem.TangemSdkError import com.tangem.commands.Card import com.tangem.commands.CardData import com.tangem.commands.CardStatus import com.tangem.commands.Command +import com.tangem.commands.common.CardDeserializer import com.tangem.commands.personalization.entities.* import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu @@ -14,9 +16,7 @@ import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.hexToBytes -import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag import com.tangem.crypto.sign @@ -39,74 +39,31 @@ class PersonalizeCommand( private val acquirer: Acquirer? = null ) : Command() { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status != CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.AlreadyPersonalized())) - return true + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + val encryptionMode = session.environment.encryptionMode + val encryptionKey = session.environment.encryptionKey + session.environment.encryptionMode = EncryptionMode.NONE + session.environment.encryptionKey = devPersonalizationKey + super.run(session) { result -> + session.environment.encryptionMode = encryptionMode + session.environment.encryptionKey = encryptionKey + callback(result) } - return false + } + + override fun performPreCheck(card: Card): TangemSdkError? { + if (card.status != CardStatus.NotPersonalized) { + return TangemSdkError.AlreadyPersonalized() + } + return null } 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 { - val tlvData = apdu.getTlvData(devPersonalizationKey) - ?: throw TangemSdkError.DeserializeApduFailed() - - val decoder = TlvDecoder(tlvData) - return Card( - cardId = decoder.decodeOptional(TlvTag.CardId) ?: "", - manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "", - status = decoder.decodeOptional(TlvTag.Status), - - firmwareVersion = decoder.decodeOptional(TlvTag.Firmware), - cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey), - settingsMask = decoder.decodeOptional(TlvTag.SettingsMask), - issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey), - curve = decoder.decodeOptional(TlvTag.CurveId), - maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures), - signingMethods = decoder.decodeOptional(TlvTag.SigningMethod), - pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2), - walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey), - walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures), - walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes), - health = decoder.decodeOptional(TlvTag.Health), - isActivated = decoder.decode(TlvTag.IsActivated), - activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed), - paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion), - userCounter = decoder.decodeOptional(TlvTag.UserCounter), - userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter), - terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked), - - cardData = deserializeCardData(tlvData) - ) - } - - private fun deserializeCardData(tlvData: List): CardData? { - val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let { - Tlv.deserialize(it.value) - } - if (cardDataTlvs.isNullOrEmpty()) return null - - val decoder = TlvDecoder(cardDataTlvs) - return CardData( - batchId = decoder.decodeOptional(TlvTag.Batch), - manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime), - issuerName = decoder.decodeOptional(TlvTag.IssuerId), - blockchainName = decoder.decodeOptional(TlvTag.BlockchainId), - manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature), - productMask = decoder.decodeOptional(TlvTag.ProductMask), - - tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol), - tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress), - tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal) - ) + return CardDeserializer.deserialize(apdu) } private fun serializePersonalizationData(config: CardConfig): ByteArray { diff --git a/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt b/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt index e2d37df4bb..a152a5f2d2 100644 --- a/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt +++ b/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt @@ -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,20 @@ 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 (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 diff --git a/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt b/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt index e7e8625384..afc869f64a 100644 --- a/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt +++ b/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt @@ -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,37 @@ 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? { + fun getTlvData(): List? { 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 + + //nothing to decrypt + if (data.size < 18) return this + + 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]) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt b/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt index ab3d97445c..1360914241 100644 --- a/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt +++ b/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt @@ -1,5 +1,6 @@ package com.tangem.crypto +import com.tangem.EncryptionMode import org.spongycastle.jce.interfaces.ECPublicKey import java.security.KeyPair import java.security.KeyPairGenerator @@ -11,6 +12,16 @@ interface EncryptionHelper { val keyA: ByteArray fun generateSecret(keyB: ByteArray): ByteArray + + companion object { + fun create(encryptionMode: EncryptionMode): EncryptionHelper? { + return when (encryptionMode) { + EncryptionMode.NONE -> null + EncryptionMode.FAST -> FastEncryptionHelper() + EncryptionMode.STRONG -> StrongEncryptionHelper() + } + } + } } class StrongEncryptionHelper : EncryptionHelper { diff --git a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt index c05384d5a1..8b9d8f2452 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt @@ -2,8 +2,10 @@ package com.tangem.tasks import com.tangem.CardSession import com.tangem.CardSessionRunnable +import com.tangem.TagType import com.tangem.TangemSdkError import com.tangem.commands.* +import com.tangem.commands.common.CardDeserializer import com.tangem.common.CompletionResult /** @@ -13,31 +15,58 @@ import com.tangem.common.CompletionResult */ internal class ScanTask : CardSessionRunnable { - override val performPreflightRead = true + override val performPreflightRead = false override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val card = session.environment.card - if (card == null) { - callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) + if (session.connectedTag == TagType.Slix) { + readSlixTag(session, callback) + return + } - } else if (card.cardData?.productMask?.contains(Product.Tag) != false) { - callback(CompletionResult.Success(card)) + ReadCommand().run(session) { readResult -> + when(readResult) { + is CompletionResult.Failure -> callback(readResult) + is CompletionResult.Success -> { + val card = readResult.data + session.environment.card = card + if (card.cardData?.productMask?.contains(Product.Tag) != false) { + callback(CompletionResult.Success(card)) - } else if (card.status != CardStatus.Loaded) { - callback(CompletionResult.Success(card)) + } else if (card.status != CardStatus.Loaded) { + callback(CompletionResult.Success(card)) - } else if (card.curve == null || card.walletPublicKey == null) { - callback(CompletionResult.Failure(TangemSdkError.CardError())) + } else if (card.curve == null || card.walletPublicKey == null) { + callback(CompletionResult.Failure(TangemSdkError.CardError())) - } else { - val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey) + } else { + val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey) + checkWalletCommand.run(session) { result -> + when (result) { + is CompletionResult.Success -> callback(CompletionResult.Success(card)) + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + } + } + } + } - checkWalletCommand.run(session) { result -> - when (result) { - is CompletionResult.Success -> callback(CompletionResult.Success(card)) - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + private fun readSlixTag(session: CardSession, callback: (result: CompletionResult) -> Unit) { + session.readSlixTag { result -> + when (result) { + is CompletionResult.Success -> { + try { + val card = CardDeserializer.deserialize(result.data) + callback(CompletionResult.Success(card)) + } catch (error: TangemSdkError) { + callback(CompletionResult.Failure(error)) + } + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) } } } diff --git a/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt b/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt index f879f65e6f..b995c4de57 100644 --- a/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt +++ b/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt @@ -4,6 +4,10 @@ import com.google.common.truth.Truth.assertThat import com.tangem.SessionEnvironment import com.tangem.common.tlv.TlvBuilder import com.tangem.common.tlv.TlvTag +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.collect import org.junit.Test @@ -19,11 +23,12 @@ class CommandApduTest { tlvBuilder.serialize() ) val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 34, 16, 32, -111, -76, -47, 66, -126, 63, 125, - 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, - -45, 19, -124, -123, -55, -94, 3) + 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, + -45, 19, -124, -123, -55, -94, 3) assertThat(commandApdu.apduData) .isEqualTo(expected) + assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1,2)) } @Test @@ -42,11 +47,11 @@ class CommandApduTest { ) val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 101, 16, 32, -111, -76, -47, 66, -126, 63, - 125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, - -10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118, - 47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120, - 126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58, - -68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90) + 125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, + -10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118, + 47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120, + 126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58, + -68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90) assertThat(commandApdu.apduData) .isEqualTo(expected) diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt index a833de5d24..c3bdaf8c09 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt @@ -181,7 +181,7 @@ class PersonalizationConfig { signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData) } if (from.SigningMethod6) { - signingMethodMaskBuilder.add(SigningMethod.SignHash) + signingMethodMaskBuilder.add(SigningMethod.SignPos) } return signingMethodMaskBuilder.build() } diff --git a/tangem-sdk-android-config/.circleci/config.yml b/tangem-sdk-android-config/.circleci/config.yml old mode 100755 new mode 100644 diff --git a/tangem-sdk/build.gradle b/tangem-sdk/build.gradle index de05139eb5..085db2391b 100644 --- a/tangem-sdk/build.gradle +++ b/tangem-sdk/build.gradle @@ -39,22 +39,31 @@ android { } dependencies { + // internal implementation project(':tangem-core') - implementation fileTree(dir: 'libs', include: ['*.jar']) + + // kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin" + implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin" + + // coroutines + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7" + + // android implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.1.0' - implementation 'com.skyfishjy.ripplebackground:library:1.0.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - - implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.lifecycle:lifecycle-runtime:2.2.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" - implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin" + // misc + implementation 'com.skyfishjy.ripplebackground:library:1.0.1' implementation 'at.favre.lib:armadillo:0.9.0' + // testing testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt index d8ca2ad645..31144eae96 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt @@ -1,20 +1,10 @@ package com.tangem.tangem_sdk_new -import android.animation.ObjectAnimator -import android.app.Activity -import android.view.HapticFeedbackConstants -import android.view.View -import android.view.animation.DecelerateInterpolator -import com.google.android.material.bottomsheet.BottomSheetDialog +import androidx.fragment.app.FragmentActivity import com.tangem.* import com.tangem.common.CompletionResult -import com.tangem.tangem_sdk_new.extensions.hide -import com.tangem.tangem_sdk_new.extensions.localizedDescription -import com.tangem.tangem_sdk_new.extensions.show import com.tangem.tangem_sdk_new.nfc.NfcReader -import com.tangem.tangem_sdk_new.ui.TouchCardAnimation -import kotlinx.android.synthetic.main.layout_touch_card.* -import kotlinx.android.synthetic.main.nfc_bottom_sheet.* +import com.tangem.tangem_sdk_new.ui.NfcSessionDialog /** * Default implementation of [SessionViewDelegate]. @@ -22,148 +12,65 @@ import kotlinx.android.synthetic.main.nfc_bottom_sheet.* */ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDelegate { - lateinit var activity: Activity - private var readingDialog: BottomSheetDialog? = null + lateinit var activity: FragmentActivity + private var readingDialog: NfcSessionDialog? = null init { setLogger() } - override fun onNfcSessionStarted(cardId: String?, message: Message?) { - reader.readingCancelled = false + override fun onSessionStarted(cardId: String?, message: Message?) { postUI { showReadingDialog(activity, cardId, message) } } - private fun showReadingDialog(activity: Activity, cardId: String?, message: Message?) { + private fun showReadingDialog(activity: FragmentActivity, cardId: String?, message: Message?) { val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null) - readingDialog = BottomSheetDialog(activity) + readingDialog = NfcSessionDialog(activity) readingDialog?.setContentView(dialogView) readingDialog?.dismissWithAnimation = true readingDialog?.create() readingDialog?.setOnShowListener { - readingDialog?.rippleBackgroundNfc?.startRippleAnimation() - val nfcDeviceAntenna = TouchCardAnimation( - activity, readingDialog!!.ivHandCardHorizontal, - readingDialog!!.ivHandCardVertical, readingDialog!!.llHand, readingDialog!!.llNfc) - nfcDeviceAntenna.init() - if (cardId != null) { - readingDialog?.tvCard?.visibility = View.VISIBLE - readingDialog?.tvCardId?.visibility = View.VISIBLE - readingDialog?.tvCardId?.text = cardId - } - if (message != null) { - if (message.body != null) readingDialog?.tvTaskText?.text = message.body - if (message.header != null) readingDialog?.tvTaskTitle?.text = message.header - } - } - readingDialog?.setOnCancelListener { - reader.readingCancelled = true - reader.closeSession() + readingDialog?.show(SessionViewDelegateState.Ready(cardId, message)) } + readingDialog?.setOnCancelListener { reader.stopSession(true) } readingDialog?.show() } override fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) { postUI { - readingDialog?.lTouchCard?.hide() - readingDialog?.tvRemainingTime?.text = ms.div(100).toString() - readingDialog?.flSecurityDelay?.show() - readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay) - readingDialog?.tvTaskText?.text = - activity.getText(R.string.dialog_security_delay_description) - - performHapticFeedback() - - if (readingDialog?.pbSecurityDelay?.max != totalDurationSeconds) { - readingDialog?.pbSecurityDelay?.max = totalDurationSeconds - } - readingDialog?.pbSecurityDelay?.progress = totalDurationSeconds - ms + 100 - - val animation = ObjectAnimator.ofInt( - readingDialog?.pbSecurityDelay, - "progress", - totalDurationSeconds - ms, - totalDurationSeconds - ms + 100) - animation.duration = 500 - animation.interpolator = DecelerateInterpolator() - animation.start() + readingDialog?.show(SessionViewDelegateState.SecurityDelay(ms, totalDurationSeconds)) } } override fun onDelay(total: Int, current: Int, step: Int) { postUI { - readingDialog?.lTouchCard?.hide() - readingDialog?.flSecurityDelay?.show() - readingDialog?.tvRemainingTime?.text = (((total - current) / step) + 1).toString() - readingDialog?.tvTaskTitle?.text = "Operation in process" - readingDialog?.tvTaskText?.text = "Please hold the card firmly until the operation is completed…" - - performHapticFeedback() - - if (readingDialog?.pbSecurityDelay?.max != total) { - readingDialog?.pbSecurityDelay?.max = total - } - readingDialog?.pbSecurityDelay?.progress = current - - val animation = ObjectAnimator.ofInt( - readingDialog?.pbSecurityDelay, - "progress", - current, - current + step) - animation.duration = 300 - animation.interpolator = DecelerateInterpolator() - animation.start() + readingDialog?.show(SessionViewDelegateState.Delay(total, current, step)) } } override fun onTagLost() { - postUI { - readingDialog?.lTouchCard?.show() - readingDialog?.flSecurityDelay?.hide() - readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan) - readingDialog?.tvTaskText?.text = activity.getText(R.string.dialog_scan_text) - } + postUI { readingDialog?.show(SessionViewDelegateState.TagLost) } + } - override fun onNfcSessionCompleted(message: Message?) { - postUI { - readingDialog?.lTouchCard?.hide() - readingDialog?.flSecurityDelay?.hide() - readingDialog?.flCompletion?.show() - readingDialog?.ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp)) - if (message != null) { - if (message.body != null) readingDialog?.tvTaskText?.text = message.body - if (message.header != null) readingDialog?.tvTaskTitle?.text = message.header - } - performHapticFeedback() - } - postUI(300) { readingDialog?.dismiss() } + override fun onTagConnected() { + postUI { readingDialog?.show(SessionViewDelegateState.TagConnected) } + } + + override fun onWrongCard() { + postUI { readingDialog?.show(SessionViewDelegateState.WrongCard) } + } + + override fun onSessionStopped(message: Message?) { + postUI { readingDialog?.show(SessionViewDelegateState.Success(message)) } } override fun onError(error: TangemSdkError) { - postUI { - readingDialog?.lTouchCard?.hide() - readingDialog?.flSecurityDelay?.hide() - readingDialog?.flCompletion?.hide() - readingDialog?.flError?.show() - readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error) - readingDialog?.tvTaskText?.text = activity.getString( - R.string.error_message, - error.code.toString(), activity.getString(error.localizedDescription()) - ) - performHapticFeedback() - } + postUI { readingDialog?.show(SessionViewDelegateState.Error(error)) } } override fun onPinRequested(callback: (result: CompletionResult) -> Unit) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - private fun performHapticFeedback() { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - readingDialog?.llHeader?.isHapticFeedbackEnabled = true - readingDialog?.llHeader?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) - } + postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(callback)) } } private fun setLogger() { diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/SessionViewDelegateState.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/SessionViewDelegateState.kt new file mode 100644 index 0000000000..04ecbd2557 --- /dev/null +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/SessionViewDelegateState.kt @@ -0,0 +1,17 @@ +package com.tangem.tangem_sdk_new + +import com.tangem.Message +import com.tangem.TangemSdkError +import com.tangem.common.CompletionResult + +sealed class SessionViewDelegateState() { + data class Error(val error: TangemSdkError) : SessionViewDelegateState() + data class Success(val message: Message?) : SessionViewDelegateState() + data class SecurityDelay(val ms: Int, val totalDurationSeconds: Int) : SessionViewDelegateState() + data class Delay(val total: Int, val current: Int, val step: Int) : SessionViewDelegateState() + data class Ready(val cardId: String?, val message: Message?) : SessionViewDelegateState() + data class PinRequested(val callback: (result: CompletionResult) -> Unit) : SessionViewDelegateState() + object TagLost : SessionViewDelegateState() + object TagConnected : SessionViewDelegateState() + object WrongCard : SessionViewDelegateState() +} \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt index 09c84716e4..29c26fa50f 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt @@ -40,7 +40,7 @@ fun TangemSdkError.localizedDescription(): Int { is TangemSdkError.Pin2OrCvcRequired -> R.string.error_operation is TangemSdkError.VerificationFailed -> R.string.error_verification_failed is TangemSdkError.DataSizeTooLarge -> R.string.error_data_size_too_large - is TangemSdkError.ExendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended + is TangemSdkError.ExtendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended is TangemSdkError.MissingCounter -> R.string.error_missing_counter is TangemSdkError.OverwritingDataIsProhibited -> R.string.error_data_cannot_be_written is TangemSdkError.DataCannotBeWritten -> R.string.error_data_cannot_be_written @@ -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 } } \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt index 008f194709..277b729d24 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt @@ -8,4 +8,13 @@ internal fun View.show() { internal fun View.hide() { this.visibility = View.GONE +} + +internal fun View.show(show: Boolean) { + if (show) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.GONE + } + } \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt index 8c2a865be4..6c73836773 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt @@ -11,6 +11,7 @@ import android.nfc.tech.IsoDep import android.os.Build import android.os.Bundle import com.tangem.Log +import com.tangem.ReadingActiveListener import com.tangem.tangem_sdk_new.ui.NfcEnableDialog /** @@ -18,7 +19,16 @@ import com.tangem.tangem_sdk_new.ui.NfcEnableDialog * Launches [NfcAdapter], manages it with [Activity] lifecycle, * enables and disables Nfc Reading Mode, receives NFC [Tag]. */ -class NfcManager : NfcAdapter.ReaderCallback { +class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener { + + override var readingIsActive: Boolean = false + set(value) { + if (value) { + disableReaderMode() + enableReaderMode() + } + field = value + } val reader = NfcReader() private var activity: Activity? = null @@ -42,7 +52,7 @@ class NfcManager : NfcAdapter.ReaderCallback { override fun onTagDiscovered(tag: Tag?) { Log.i(this::class.simpleName!!, "Nfc tag is discovered") - if (reader.readingActive) reader.onTagDiscovered(tag) else ignoreTag(tag) + if (readingIsActive) reader.onTagDiscovered(tag) else ignoreTag(tag) } @@ -50,11 +60,10 @@ class NfcManager : NfcAdapter.ReaderCallback { val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED) activity?.registerReceiver(mBroadcastReceiver, filter) handleNfcEnabled(nfcAdapter?.isEnabled == true) - reader.manager = this + reader.listener = this } private fun handleNfcEnabled(nfcEnabled: Boolean) { - reader.nfcEnabled = nfcEnabled if (nfcEnabled) { enableReaderMode() nfcEnableDialog?.cancel() @@ -67,7 +76,7 @@ class NfcManager : NfcAdapter.ReaderCallback { fun onPause() { activity?.unregisterReceiver(mBroadcastReceiver) disableReaderMode() - reader.manager = null + reader.listener = null } fun onDestroy() { @@ -75,11 +84,11 @@ class NfcManager : NfcAdapter.ReaderCallback { nfcAdapter = null } - fun enableReaderMode() { + private fun enableReaderMode() { nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, Bundle()) } - fun disableReaderMode() { + private fun disableReaderMode() { nfcAdapter?.disableReaderMode(activity) } diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt index 559adf3f80..8636b5f366 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt @@ -4,127 +4,123 @@ 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.TangemSdkError +import com.tangem.* import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.ResponseApdu import com.tangem.common.extensions.toHexString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +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) /** * Provides NFC communication between an Android application and Tangem card. */ class NfcReader : CardReader { + override val tag = ConflatedBroadcastChannel() + override var scope: CoroutineScope? = null - var readingActive = false - private set - var nfcEnabled = false - var manager: NfcManager? = null - private var isoDep: IsoDep? = null - set(value) { - // don't reassign when there's an active tag already - if (field == null) { - field = value - // if tag is received, call connect first before transceiving data - if (value != null) connect() - } - if (value == null) field = value - } + var listener: ReadingActiveListener? = null - var readingCancelled = false + private var nfcTag: NfcTag? = null set(value) { field = value - if (value) { - // Stops reading and sends failure callback to a task - // if reading is cancelled (when user closes nfc bottom sheet dialog). - closeSession() - callback?.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled())) - } + scope?.launch { tag.send(value?.type) } } - private var data: ByteArray? = null - private var callback: ((response: CompletionResult) -> Unit)? = null - - override fun openSession() { + override fun startSession() { Log.i(this::class.simpleName!!, "NFC reader is starting NFC session") - readingActive = true - readingCancelled = false - manager?.disableReaderMode() - manager?.enableReaderMode() - } - - override fun closeSession() { - isoDep = null - readingActive = false - } - - override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult) -> Unit) { - data = apdu.apduData - this.callback = callback - if (isoDep != null) { - transceiveData() - } - } - - private fun transceiveData() { - if (readingCancelled) { - callback?.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled())) - return - } - if (data == null) return - - val rawResponse: ByteArray? - try { - 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()}") - rawResponse = isoDep?.transceive(data) - Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}") - } catch (exception: TagLostException) { - callback?.invoke(CompletionResult.Failure(TangemSdkError.TagLost())) - isoDep = null - return - } catch (exception: Exception) { - Log.i(this::class.simpleName!!, exception.localizedMessage ?: "Error tranceiving data") - // The messages of errors can vary on different Android devices, - // but we try to identify it by parsing the message. - if (exception.message?.contains("length") == true) { - callback?.invoke(CompletionResult.Failure(TangemSdkError.ExtendedLengthNotSupported())) - } - isoDep = null - return - } - if (rawResponse != null) { - Log.i(this::class.simpleName!!, "Data from the card was received") - data = null - } - rawResponse?.let { callback?.invoke(CompletionResult.Success(ResponseApdu(it))) } + listener?.readingIsActive = true } fun onTagDiscovered(tag: Tag?) { - NfcV.get(tag)?.let { onNfcVDiscovered(it) } - isoDep = IsoDep.get(tag) - transceiveData() + NfcV.get(tag)?.let { + nfcTag = NfcTag(TagType.Slix, null, NfcV.get(tag)) + return + } + IsoDep.get(tag)?.let { isoDep -> + connect(isoDep) + nfcTag = NfcTag(TagType.Nfc, isoDep) + } } - - private fun connect() { - isoDep?.connect() - isoDep?.close() - isoDep?.connect() - isoDep?.timeout = 240000 + private fun connect(isoDep: IsoDep) { + isoDep.connect() + isoDep.close() + isoDep.connect() + isoDep.timeout = 240000 Log.i(this::class.simpleName!!, "NFC tag is connected") } - private fun onNfcVDiscovered(nfcV: NfcV) { + override fun stopSession(cancelled: Boolean) { + nfcTag = null + listener?.readingIsActive = false + if (cancelled) scope?.cancel() + } + + override suspend fun transceiveApdu(apdu: CommandApdu): CompletionResult = + suspendCancellableCoroutine { continuation -> + transceiveApdu(apdu) { result -> + if (continuation.isActive) continuation.resume(result) + } + } + + override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult) -> Unit) { + val rawResponse: ByteArray? = try { + transcieveAndLog(apdu.apduData) + } catch (exception: TagLostException) { + callback.invoke(CompletionResult.Failure(TangemSdkError.TagLost())) + nfcTag = null + return + } catch (exception: Exception) { + tryHandleNfcError(exception, callback) + nfcTag = null + return + } + + if (rawResponse != null) { + Log.i(this::class.simpleName!!, "Data from the card was received") + } + rawResponse?.let { callback.invoke(CompletionResult.Success(ResponseApdu(it))) } + } + + 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) + Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}") + return rawResponse + } + + private fun tryHandleNfcError(exception: Exception, callback: (response: CompletionResult) -> Unit) { + Log.i(this::class.simpleName!!, exception.localizedMessage ?: "Error tranceiving data") + // The messages of errors can vary on different Android devices, + // but we try to identify it by parsing the message. + if (exception.message?.contains("length") == true) { + callback.invoke(CompletionResult.Failure(TangemSdkError.ExtendedLengthNotSupported())) + } + } + + override fun readSlixTag(callback: (result: CompletionResult) -> Unit) { + val nfcV = nfcTag?.nfcV + if (nfcV == null) { + callback.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand())) + return + } val response = SlixTagReader().transceive(nfcV) when (response) { is SlixReadResult.Failure -> { Log.e(this::class.simpleName!!, "${response.exception.message}") - callback?.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand())) + callback.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand())) } is SlixReadResult.Success -> { - callback?.invoke(CompletionResult.Success(ResponseApdu(response.data))) + callback.invoke(CompletionResult.Success(ResponseApdu(response.data))) } } } diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/ui/NfcSessionDialog.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/ui/NfcSessionDialog.kt new file mode 100644 index 0000000000..4e7468f390 --- /dev/null +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/ui/NfcSessionDialog.kt @@ -0,0 +1,171 @@ +package com.tangem.tangem_sdk_new.ui + +import android.animation.ObjectAnimator +import android.view.HapticFeedbackConstants +import android.view.View +import android.view.animation.DecelerateInterpolator +import androidx.fragment.app.FragmentActivity +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.tangem.tangem_sdk_new.R +import com.tangem.tangem_sdk_new.SessionViewDelegateState +import com.tangem.tangem_sdk_new.extensions.localizedDescription +import com.tangem.tangem_sdk_new.extensions.show +import com.tangem.tangem_sdk_new.postUI +import kotlinx.android.synthetic.main.layout_touch_card.* +import kotlinx.android.synthetic.main.nfc_bottom_sheet.* + +class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activity) { + + private var currentState: SessionViewDelegateState? = null + + fun show(state: SessionViewDelegateState) { + when (state) { + is SessionViewDelegateState.Ready -> onReady(state) + is SessionViewDelegateState.Success -> onSuccess(state) + is SessionViewDelegateState.Error -> onError(state) + is SessionViewDelegateState.SecurityDelay -> onSecurityDelay(state) + is SessionViewDelegateState.Delay -> onDelay(state) + is SessionViewDelegateState.PinRequested -> onPinRequested(state) + is SessionViewDelegateState.TagLost -> onTagLost() + is SessionViewDelegateState.TagConnected -> onTagConnected() + is SessionViewDelegateState.WrongCard -> onWrongCard() + } + currentState = state + } + + private fun onReady(state: SessionViewDelegateState.Ready) { + show(lTouchCard) + rippleBackgroundNfc?.startRippleAnimation() + val nfcDeviceAntenna = TouchCardAnimation( + activity, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc + ) + nfcDeviceAntenna.init() + state.cardId?.let { cardId -> + tvCard?.show() + tvCardId?.show() + tvCardId?.text = cardId + } + state.message?.let { message -> + if (message.body != null) tvTaskText?.text = message.body + if (message.header != null) tvTaskTitle?.text = message.header + } + } + + private fun onSuccess(state: SessionViewDelegateState.Success) { + show(flCompletion) + ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp)) + state.message?.let { message -> + if (message.body != null) tvTaskText?.text = message.body + if (message.header != null) tvTaskTitle?.text = message.header + } + performHapticFeedback() + postUI(300) { dismiss() } + } + + private fun onError(state: SessionViewDelegateState.Error) { + show(flError) + tvTaskTitle?.text = activity.getText(R.string.dialog_error) + tvTaskText?.text = activity.getString( + R.string.error_message, + state.error.code.toString(), activity.getString(state.error.localizedDescription()) + ) + performHapticFeedback() + } + + private fun onSecurityDelay(state: SessionViewDelegateState.SecurityDelay) { + show(flSecurityDelay) + + tvRemainingTime?.text = state.ms.div(100).toString() + tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay) + tvTaskText?.text = + activity.getText(R.string.dialog_security_delay_description) + + performHapticFeedback() + + if (pbSecurityDelay?.max != state.totalDurationSeconds) { + pbSecurityDelay?.max = state.totalDurationSeconds + } + pbSecurityDelay?.progress = state.totalDurationSeconds - state.ms + 100 + + val animation = ObjectAnimator.ofInt( + pbSecurityDelay, + "progress", + state.totalDurationSeconds - state.ms, + state.totalDurationSeconds - state.ms + 100) + animation.duration = 500 + animation.interpolator = DecelerateInterpolator() + animation.start() + } + + private fun onDelay(state: SessionViewDelegateState.Delay) { + show(flSecurityDelay) + tvRemainingTime?.text = (((state.total - state.current) / state.step) + 1).toString() + tvTaskTitle?.text = "Operation in process" + tvTaskText?.text = "Please hold the card firmly until the operation is completed…" + + performHapticFeedback() + + if (pbSecurityDelay?.max != state.total) { + pbSecurityDelay?.max = state.total + } + pbSecurityDelay?.progress = state.current + + val animation = ObjectAnimator.ofInt( + pbSecurityDelay, + "progress", + state.current, + state.current + state.step) + animation.duration = 300 + animation.interpolator = DecelerateInterpolator() + animation.start() + } + + private fun onPinRequested(state: SessionViewDelegateState.PinRequested) { + TODO("To be implemented") + } + + private fun onTagLost() { + if (currentState is SessionViewDelegateState.Success) return + show(lTouchCard) + tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan) + tvTaskText?.text = activity.getText(R.string.dialog_scan_text) + } + + private fun onTagConnected() { + show(flReading) + } + + private fun onWrongCard() { + if (currentState !is SessionViewDelegateState.WrongCard) { + show(flError) + tvTaskTitle?.text = activity.getText(R.string.dialog_error) + tvTaskText?.text = activity.getString( + R.string.error_message, + "Wrong Card", activity.getString(R.string.error_wrong_card_number) + ) + performHapticFeedback() + + postUI (2000){ + show(lTouchCard) + tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan) + tvTaskText?.text = activity.getText(R.string.dialog_scan_text) + } + + } + } + + private fun show(view: View) { + lTouchCard?.show(view.id == lTouchCard.id) + flSecurityDelay?.show(view.id == flSecurityDelay.id) + flReading?.show(view.id == flReading.id) + flError?.show(view.id == flError.id) + flCompletion?.show(view.id == flCompletion.id) + } + + private fun performHapticFeedback() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + llHeader?.isHapticFeedbackEnabled = true + llHeader?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + } + } +} \ No newline at end of file diff --git a/tangem-sdk/src/main/res/drawable/pb_simple_circle.xml b/tangem-sdk/src/main/res/drawable/pb_simple_circle.xml new file mode 100644 index 0000000000..1ae7f2d76a --- /dev/null +++ b/tangem-sdk/src/main/res/drawable/pb_simple_circle.xml @@ -0,0 +1,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml b/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml index 33bf9ad0a3..55317c459a 100644 --- a/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml +++ b/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml @@ -157,9 +157,24 @@ android:progressDrawable="@drawable/pb_circle" android:progressTint="#FF9D30" android:secondaryProgress="100" /> - + + + + + You tapped a different card. Please match the in-app Card ID to the your physical Card ID to continue this process. This card is configured for a different app. Please see the information on your card and download the related app. Your card is missing essential data + Invalid Response \ No newline at end of file