From 8c8d223c727325f707c57e13ed4ea43321013195 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2020 12:09:01 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/{common => }/CardEnvironment.kt | 11 +- .../src/main/java/com/tangem/CardManager.kt | 330 --------------- .../src/main/java/com/tangem/CardSession.kt | 221 ++++++++++ ...Delegate.kt => CardSessionViewDelegate.kt} | 16 +- .../src/main/java/com/tangem/Config.kt | 22 + .../src/main/java/com/tangem/SessionError.kt | 139 +++++++ .../src/main/java/com/tangem/TangemSdk.kt | 383 ++++++++++++++++++ .../com/tangem/commands/CheckWalletCommand.kt | 65 ++- .../main/java/com/tangem/commands/Command.kt | 117 ++++++ .../com/tangem/commands/CommandSerializer.kt | 44 -- .../tangem/commands/CreateWalletCommand.kt | 41 +- .../com/tangem/commands/OpenSessionCommand.kt | 31 +- .../com/tangem/commands/PurgeWalletCommand.kt | 35 +- .../java/com/tangem/commands/ReadCommand.kt | 103 ++--- .../tangem/commands/ReadIssuerDataCommand.kt | 76 +++- .../commands/ReadIssuerExtraDataCommand.kt | 125 ++++-- .../tangem/commands/ReadUserDataCommand.kt | 39 +- .../java/com/tangem/commands/SignCommand.kt | 68 ++-- .../tangem/commands/WriteIssuerDataCommand.kt | 73 +++- .../commands/WriteIssuerExtraDataCommand.kt | 111 ++++- .../tangem/commands/WriteUserDataCommand.kt | 64 ++- .../personalization/DepersonalizeCommand.kt | 11 +- .../personalization/PersonalizeCommand.kt | 92 ++--- .../personalization/entities/Acquirer.kt | 2 +- .../personalization/entities/Issuer.kt | 2 +- .../personalization/entities/Manufacturer.kt | 2 +- .../com/tangem/common/CompletionResult.kt | 4 +- .../com/tangem/common/TerminalKeysService.kt | 6 +- .../com/tangem/common/apdu/CommandApdu.kt | 2 +- .../java/com/tangem/common/apdu/StatusWord.kt | 17 +- .../tlv/{TlvMapper.kt => TlvDecoder.kt} | 28 +- .../java/com/tangem/common/tlv/TlvEncoder.kt | 7 +- .../java/com/tangem/tasks/CreateWalletTask.kt | 43 ++ .../com/tangem/tasks/ReadIssuerDataTask.kt | 53 --- .../tangem/tasks/ReadIssuerExtraDataTask.kt | 100 ----- .../main/java/com/tangem/tasks/ScanTask.kt | 72 +--- .../com/tangem/tasks/SingleCommandTask.kt | 39 -- .../src/main/java/com/tangem/tasks/Task.kt | 358 ---------------- .../com/tangem/tasks/WriteIssuerDataTask.kt | 67 --- .../tangem/tasks/WriteIssuerExtraDataTask.kt | 110 ----- .../{TlvMapperTest.kt => TlvDecoderTest.kt} | 70 ++-- ...e.kt => DefaultCardSessionViewDelegate.kt} | 28 +- .../tangem_sdk_new/TerminalKeysStorage.kt | 4 +- .../{CardManager.kt => TangemSdk.kt} | 10 +- .../tangem/tangem_sdk_new/nfc/NfcReader.kt | 10 +- .../tangem_sdk_new/nfc/SlixTagReader.kt | 6 +- 46 files changed, 1657 insertions(+), 1600 deletions(-) rename tangem-core/src/main/java/com/tangem/{common => }/CardEnvironment.kt (76%) delete mode 100644 tangem-core/src/main/java/com/tangem/CardManager.kt create mode 100644 tangem-core/src/main/java/com/tangem/CardSession.kt rename tangem-core/src/main/java/com/tangem/{CardManagerDelegate.kt => CardSessionViewDelegate.kt} (78%) create mode 100644 tangem-core/src/main/java/com/tangem/SessionError.kt create mode 100644 tangem-core/src/main/java/com/tangem/TangemSdk.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/Command.kt delete mode 100644 tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt rename tangem-core/src/main/java/com/tangem/common/tlv/{TlvMapper.kt => TlvDecoder.kt} (86%) create mode 100644 tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt delete mode 100644 tangem-core/src/main/java/com/tangem/tasks/ReadIssuerDataTask.kt delete mode 100644 tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt delete mode 100644 tangem-core/src/main/java/com/tangem/tasks/SingleCommandTask.kt delete mode 100644 tangem-core/src/main/java/com/tangem/tasks/Task.kt delete mode 100644 tangem-core/src/main/java/com/tangem/tasks/WriteIssuerDataTask.kt delete mode 100644 tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt rename tangem-core/src/test/java/com/tangem/common/tlv/{TlvMapperTest.kt => TlvDecoderTest.kt} (71%) rename tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/{DefaultCardManagerDelegate.kt => DefaultCardSessionViewDelegate.kt} (87%) rename tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/{CardManager.kt => TangemSdk.kt} (62%) diff --git a/tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt b/tangem-core/src/main/java/com/tangem/CardEnvironment.kt similarity index 76% rename from tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt rename to tangem-core/src/main/java/com/tangem/CardEnvironment.kt index 09cfd736b4..e4b2c36c55 100644 --- a/tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt +++ b/tangem-core/src/main/java/com/tangem/CardEnvironment.kt @@ -1,5 +1,6 @@ -package com.tangem.common +package com.tangem +import com.tangem.commands.Card import com.tangem.commands.EllipticCurve import com.tangem.crypto.CryptoUtils.generatePublicKey @@ -7,11 +8,14 @@ import com.tangem.crypto.CryptoUtils.generatePublicKey /** * Contains data relating to a Tangem card. It is used in constructing all the commands, * and commands can return modified [CardEnvironment]. + * + * @property card Current card, read by preflight [com.tangem.commands.ReadCommand]. + * @property terminalKeys generated terminal keys used in Linked Terminal feature. */ data class CardEnvironment( val pin1: String = DEFAULT_PIN, val pin2: String = DEFAULT_PIN2, - val cardId: String? = null, + var card: Card? = null, val terminalKeys: KeyPair? = null, var encryptionMode: EncryptionMode = EncryptionMode.NONE, var encryptionKey: ByteArray? = null, @@ -24,6 +28,9 @@ data class CardEnvironment( } } +/** + * All possible encryption modes. + */ enum class EncryptionMode(val code: Byte) { NONE(0x0), FAST(0x1), diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt deleted file mode 100644 index c5a1f99e77..0000000000 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ /dev/null @@ -1,330 +0,0 @@ -package com.tangem - -import com.tangem.commands.* -import com.tangem.commands.personalization.DepersonalizeCommand -import com.tangem.commands.personalization.DepersonalizeResponse -import com.tangem.commands.personalization.PersonalizeCommand -import com.tangem.commands.personalization.entities.Acquirer -import com.tangem.commands.personalization.entities.CardConfig -import com.tangem.commands.personalization.entities.Issuer -import com.tangem.commands.personalization.entities.Manufacturer -import com.tangem.common.CardEnvironment -import com.tangem.common.TerminalKeysService -import com.tangem.crypto.CryptoUtils -import com.tangem.tasks.* - -/** - * The main interface of Tangem SDK that allows your app to communicate with Tangem cards. - * - * @property reader is an interface that is responsible for NFC connection and - * transfer of data to and from the Tangem Card. - * Its default implementation, NfcCardReader, is in our tangem-sdk module. - * @property cardManagerDelegate An interface that allows interaction with users and shows relevant UI. - * Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module. - */ -class CardManager( - private val reader: CardReader, - private val cardManagerDelegate: CardManagerDelegate? = null, - private val config: Config = Config() -) { - - private var terminalKeysService: TerminalKeysService? = null - private var isBusy = false - - init { - CryptoUtils.initCrypto() - } - - /** - * To start using any card, you first need to read it using the scanCard() method. - * This method launches an NFC session, and once it’s connected with the card, - * it obtains the card data. Optionally, if the card contains a wallet (private and public key pair), - * it proves that the wallet owns a private key that corresponds to a public one. - * - * It launches on the new thread a [ScanTask] that will send the following events in a callback: - * [ScanEvent.OnReadEvent] after completing [com.tangem.commands.ReadCommand] - * [ScanEvent.OnVerifyEvent] after completing [com.tangem.commands.CheckWalletCommand] - * [TaskEvent.Completion] with an error field null after successful completion of a task or - * [TaskEvent.Completion] with a [TaskError] if some error occurs. - * @param callback is triggered on events during a performance of the task, - * provides data in form of [ScanEvent] subclasses. - */ - fun scanCard(callback: (result: TaskEvent) -> Unit) { - val task = ScanTask() - runTask(task, callback = callback) - } - - /** - * This method allows you to sign one or multiple hashes. - * Simultaneous signing of array of hashes in a single [SignCommand] is required to support - * Bitcoin-type multi-input blockchains (UTXO). - * The [SignCommand] will return a corresponding array of signatures. - * - * This method launches on the new thread [SignCommand] that will send the following events in a callback: - * [SignResponse] after completing [SignCommand] - * [TaskEvent.Completion] with an error field null after successful completion of a task or - * [TaskEvent.Completion] with a [TaskError] if some error occurs. - * Please note that Tangem cards usually protect the signing with a security delay - * that may last up to 90 seconds, depending on a card. - * It is for [CardManagerDelegate] to notify users of security delay. - * @param hashes Array of transaction hashes. It can be from one or up to ten hashes of the same length. - * @param cardId CID, Unique Tangem card ID number - * @param callback is triggered on the completion of the [SignCommand], - * provides card response in the form of [SignResponse]. - */ - fun sign(hashes: Array, cardId: String, - callback: (result: TaskEvent) -> Unit) { - val signCommand = SignCommand(hashes) - val task = SingleCommandTask(signCommand) - runTask(task, cardId, callback) - } - - /** - * This command returns 512-byte Issuer Data field and its issuer’s signature. - * Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, - * format and payload of Issuer Data. For example, this field may contain information about - * wallet balance signed by the issuer or additional issuer’s attestation data. - * @param cardId CID, Unique Tangem card ID number. - * @param callback is triggered on the completion of the [ReadIssuerDataCommand], - * provides card response in the form of [ReadIssuerDataResponse]. - */ - fun readIssuerData(cardId: String, - callback: (result: TaskEvent) -> Unit) { - val task = ReadIssuerDataTask(config.issuerPublicKey) - runTask(task, cardId, callback) - } - - /** - * This task retrieves Issuer Extra Data field and its issuer’s signature. - * Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, - * format and payload of Issuer Data. . For example, this field may contain photo or - * biometric information for ID card product. Because of the large size of Issuer_Extra_Data, - * a series of these commands have to be executed to read the entire Issuer_Extra_Data. - * @param cardId CID, Unique Tangem card ID number. - * @param callback is triggered on the completion of the [ReadIssuerExtraDataTask], - * provides card response in the form of [ReadIssuerExtraDataResponse]. - */ - fun readIssuerExtraData(cardId: String, - callback: (result: TaskEvent) -> Unit) { - val task = ReadIssuerExtraDataTask(config.issuerPublicKey) - runTask(task, cardId, callback) - } - - /** - * This command writes 512-byte Issuer Data field and its issuer’s signature. - * Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, - * format and payload of Issuer Data. For example, this field may contain information about - * wallet balance signed by the issuer or additional issuer’s attestation data. - * @param cardId CID, Unique Tangem card ID number. - * @param issuerData Data provided by issuer. - * @param issuerDataSignature Issuer’s signature of [issuerData] with Issuer Data Private Key. - * @param issuerDataCounter An optional counter that protect issuer data against replay attack. - * @param callback is triggered on the completion of the [WriteIssuerDataCommand], - * provides card response in the form of [WriteIssuerDataResponse]. - */ - fun writeIssuerData(cardId: String, - issuerData: ByteArray, - issuerDataSignature: ByteArray, - issuerDataCounter: Int? = null, - callback: (result: TaskEvent) -> Unit) { - val task = WriteIssuerDataTask( - issuerData, - issuerDataSignature, - issuerDataCounter, - config.issuerPublicKey - ) - runTask(task, cardId, callback) - } - - /** - * This task writes Issuer Extra Data field and its issuer’s signature. - * Issuer Extra Data is never changed or parsed from within the Tangem COS. - * The issuer defines purpose of use, format and payload of Issuer Data. - * For example, this field may contain a photo or biometric information for ID card products. - * Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed - * to write entire Issuer_Extra_Data. - * @param cardId CID, Unique Tangem card ID number. - * @param issuerData Data provided by issuer. - * @param startingSignature Issuer’s signature with Issuer Data Private Key of [cardId], - * [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and - * Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData]. - * @param finalizingSignature Issuer’s signature with Issuer Data Private Key of [cardId], - * [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay - * andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]). - * @param issuerDataCounter An optional counter that protect issuer data against replay attack. - * @param callback is triggered on the completion of the [WriteIssuerDataCommand], - * provides card response in the form of [WriteIssuerDataResponse]. - */ - fun writeIssuerExtraData(cardId: String, - issuerData: ByteArray, - startingSignature: ByteArray, - finalizingSignature: ByteArray, - issuerDataCounter: Int? = null, - callback: (result: TaskEvent) -> Unit) { - val task = WriteIssuerExtraDataTask( - issuerData, - startingSignature, finalizingSignature, - config.issuerPublicKey, - issuerDataCounter - ) - runTask(task, cardId, callback) - } - - /** - * This command write some of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields. - * User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS. - * The App defines purpose of use, format and it's payload. For example, this field may contain cashed information - * from blockchain to accelerate preparing new transaction. - * User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing - * of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use. - * For example, this fields may contain blockchain nonce value. - * - * Writing of User_Counter and User_Data protected only by PIN1. - * User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation. - */ - fun writeUserData( - cardId: String, - userData: ByteArray? = null, - userProtectedData: ByteArray? = null, - userCounter: Int? = null, - userProtectedCounter: Int? = null, - callback: (result: TaskEvent) -> Unit - ) { - val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter) - val task = SingleCommandTask(writeUserDataCommand) - runTask(task, cardId, callback) - } - - /** - * This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and - * User_Protected_Counter fields. - * User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS. - * The App defines purpose of use, format and it's payload. For example, this field may contain cashed information - * from blockchain to accelerate preparing new transaction. - * User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing - * of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use. - * For example, this fields may contain blockchain nonce value. - */ - fun readUserData(cardId: String, callback: (result: TaskEvent) -> Unit) { - val task = SingleCommandTask(ReadUserDataCommand()) - runTask(task, cardId, callback) - } - - /** - * This command will create a new wallet on the card having ‘Empty’ state. - * A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card. - * App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand] - * and then transform it into an address of corresponding blockchain wallet - * according to a specific blockchain algorithm. - * WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand]. - * RemainingSignature is set to MaxSignatures. - * @param cardId CID, Unique Tangem card ID number. - */ - fun createWallet(cardId: String, - callback: (result: TaskEvent) -> Unit) { - val createWalletCommand = CreateWalletCommand() - val task = SingleCommandTask(createWalletCommand) - runTask(task, cardId, callback) - } - - /** - * This command deletes all wallet data. If Is_Reusable flag is enabled during personalization, - - * If Is_Reusable flag is disabled, the card switches to ‘Purged’ state. - * ‘Purged’ state is final, it makes the card useless. - * @param cardId CID, Unique Tangem card ID number. - */ - fun purgeWallet(cardId: String, - callback: (result: TaskEvent) -> Unit) { - val purgeWalletCommand = PurgeWalletCommand() - val task = SingleCommandTask(purgeWalletCommand) - runTask(task, cardId, callback) - } - - /** - * Command available on SDK cards only - * - * This command resets card to initial state, - * erasing all data written during personalization and usage. - * @param cardId CID, Unique Tangem card ID number. - */ - fun depersonalize(cardId: String, - callback: (result: TaskEvent) -> Unit) { - val depersonalizeCommand = DepersonalizeCommand() - val task = SingleCommandTask(depersonalizeCommand) - runTask(task, cardId, callback) - } - - /** - * Command available on SDK cards only - * - * Personalization is an initialization procedure, required before starting using a card. - * During this procedure a card setting is set up. - * During this procedure all data exchange is encrypted. - * @param config is a configuration file with all the card settings that are written on the card - * during personalization. - * @param issuer Issuer is a third-party team or company wishing to use Tangem cards. - * @param manufacturer Tangem Card Manufacturer. - * @param acquirer Acquirer is a trusted third-party company that operates proprietary - * (non-EMV) POS terminal infrastructure and transaction processing back-end. - */ - fun personalize(config: CardConfig, - issuer: Issuer, manufacturer: Manufacturer, acquirer: Acquirer? = null, - callback: (result: TaskEvent) -> Unit) { - val personalizationCommand = PersonalizeCommand(config, issuer, manufacturer, acquirer) - val task = SingleCommandTask(personalizationCommand) - task.performPreflightRead = false - runTask(task, callback = callback) - } - - /** - - */ - fun runTask(task: Task, cardId: String? = null, callback: (result: TaskEvent) -> Unit) { - if (isBusy) { - callback(TaskEvent.Completion(TaskError.Busy())) - return - } - - val environment = prepareCardEnvironment(cardId) - isBusy = true - - task.reader = reader - task.delegate = cardManagerDelegate - - Thread().run { - task.run(environment) { taskEvent -> - if (taskEvent is TaskEvent.Completion) isBusy = false - callback(taskEvent) - } - } - } - - /** - - */ - fun runCommand(command: CommandSerializer, - cardId: String? = null, - callback: (result: TaskEvent) -> Unit) { - val task = SingleCommandTask(command) - runTask(task, cardId, callback) - } - - /** - * Allows to set a particular [TerminalKeysService] to retrieve terminal keys. - * Default implementation is provided in tangem-sdk module: [TerminalKeysStorage]. - */ - fun setTerminalKeysService(terminalKeysService: TerminalKeysService) { - this.terminalKeysService = terminalKeysService - } - - private fun prepareCardEnvironment(cardId: String?): CardEnvironment { - val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null - return CardEnvironment( - cardId = cardId, - terminalKeys = terminalKeys - ) - } - - companion object -} \ 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 new file mode 100644 index 0000000000..f8455ace3e --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/CardSession.kt @@ -0,0 +1,221 @@ +package com.tangem + +import com.tangem.commands.Card +import com.tangem.commands.CommandResponse +import com.tangem.commands.OpenSessionCommand +import com.tangem.commands.ReadCommand +import com.tangem.common.CompletionResult +import com.tangem.common.apdu.CommandApdu +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.extensions.calculateSha256 +import com.tangem.crypto.EncryptionHelper +import com.tangem.crypto.FastEncryptionHelper +import com.tangem.crypto.StrongEncryptionHelper +import com.tangem.crypto.pbkdf2Hash + +/** + * Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession] + */ +interface CardSessionRunnable { + + /** + * The starting point for custom business logic. + * Implement this interface and use [TangemSdk.startSession] to run. + * @param session run commands in this [CardSession]. + * @param callback trigger the callback to complete the task. + */ + fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) +} + +/** + * Allows interaction with Tangem cards. Should be opened before sending commands. + * + * @property environment + * @property reader is an interface that is responsible for NFC connection and + * transfer of data to and from the Tangem Card. + * @property viewDelegate is an interface that allows interaction with users and shows relevant UI. + * @property cardId ID, Unique Tangem card ID number. If not null, the SDK will check that you the card + * with which you tapped a phone has this [cardId] and SDK will return + * the [SessionError.WrongCard] otherwise. + * @property initialMessage A custom description that will be shown at the beginning of the NFC session. + * If null, a default header and text body will be used. + */ +class CardSession( + val environment: CardEnvironment, + val reader: CardReader, + val viewDelegate: CardSessionViewDelegate, + private var cardId: String? = null, + private val initialMessage: Message? = null +) { + + /** + * True if some operation is still in progress. + */ + private var isBusy = false + + /** + * This metod starts a card session, performs preflight [ReadCommand], + * invokes [CardSessionRunnable.run] and closes the session. + * @param runnable [CardSessionRunnable] that will be performed in the session. + * @param callback will be triggered with a [CompletionResult] of a session. + */ + fun , R : CommandResponse> startWithRunnable( + runnable: T, callback: (result: CompletionResult) -> Unit) { + + start { 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 -> + stop() + callback(result) + } + } + } + + /** + * Starts a card session and performs preflight [ReadCommand]. + * @param callback: callback with the card session. Can contain [SessionError] if something goes wrong. + */ + fun start(callback: (session: CardSession, error: SessionError?) -> Unit) { + try { + startSession() + } catch (error: SessionError) { + callback(this, error) + } + + preflightRead() { result -> + when (result) { + is CompletionResult.Failure -> { + callback(this, result.error) + stopWithError(result.error) + } + is CompletionResult.Success -> { + callback(this, null) + + } + } + } + } + + private fun startSession() { + if (isBusy) throw SessionError.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(SessionError.WrongCard()) + callback(CompletionResult.Failure(SessionError.WrongCard())) + return@run + } + environment.card = result.data + cardId = receivedCardId + callback(CompletionResult.Success(result.data)) + } + } + } + } + + /** + * Stops the current session with the text message. + * @param message If null, the default message will be shown. + */ + private fun stop(message: Message? = null) { + reader.closeSession() + viewDelegate.onNfcSessionCompleted(message) + isBusy = false + } + + /** + * Stops the current session on error. + * @param error An error that will be shown. + */ + private fun stopWithError(error: Exception) { + reader.closeSession() + isBusy = false + + val errorMessage = if (error is SessionError) { + "${error::class.simpleName}: ${error.code}" + } else { + error.localizedMessage + } + if (error !is SessionError.UserCancelled) viewDelegate.onError(errorMessage) + } + + fun send(apdu: CommandApdu, callback: (result: CompletionResult) -> Unit) { + reader.transceiveApdu(apdu, callback) + } + + private fun tryHandleError( + error: SessionError, callback: (result: CompletionResult) -> Unit) { + + when (error) { + is SessionError.NeedEncryption -> { + 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(this::class.simpleName!!, "Encryption doesn't work") + callback(CompletionResult.Failure(SessionError.NeedEncryption())) + } + } + return establishEncryption(callback) + } + else -> callback(CompletionResult.Failure(SessionError.UnknownError())) + } + } + + private fun establishEncryption(callback: (result: CompletionResult) -> Unit) { + val encryptionHelper: EncryptionHelper = + if (environment.encryptionMode == EncryptionMode.STRONG) { + StrongEncryptionHelper() + } else { + FastEncryptionHelper() + } + val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA) + openSesssionCommand.run(this) { result -> + when (result) { + is CompletionResult.Success -> { + val uid = result.data.uid + val protocolKey = environment.pin1.calculateSha256().pbkdf2Hash(uid, 50) + val secret = encryptionHelper.generateSecret(result.data.sessionKeyB) + val sessionKey = (secret + protocolKey).calculateSha256() + environment.encryptionKey = sessionKey + callback(CompletionResult.Success(true)) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt b/tangem-core/src/main/java/com/tangem/CardSessionViewDelegate.kt similarity index 78% rename from tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt rename to tangem-core/src/main/java/com/tangem/CardSessionViewDelegate.kt index 5fce77186d..da5f26e020 100644 --- a/tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt +++ b/tangem-core/src/main/java/com/tangem/CardSessionViewDelegate.kt @@ -1,19 +1,18 @@ package com.tangem import com.tangem.common.CompletionResult -import com.tangem.tasks.TaskError /** * Allows interaction with users and shows visual elements. * * Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module. */ -interface CardManagerDelegate { +interface CardSessionViewDelegate { /** * It is called when user is expected to scan a Tangem Card with an Android device. */ - fun onNfcSessionStarted(cardId: String?) + fun onNfcSessionStarted(cardId: String?, message: Message? = null) /** * It is called when security delay is triggered by the card. @@ -36,16 +35,21 @@ interface CardManagerDelegate { /** * It is called when NFC session was completed and a user can take the card away from the Android device. */ - fun onNfcSessionCompleted() + fun onNfcSessionCompleted(message: Message? = null) /** * It is called when some error occur during NFC session. */ - fun onError(error: TaskError) + fun onError(errorMessage: String) /** * It is called when a user is expected to enter pin code. */ fun onPinRequested(callback: (result: CompletionResult) -> Unit) -} \ No newline at end of file +} + +/** + * Wrapper for a message that can be shown to user after a start of NFC session. + */ +data class Message(val header: String? = null, val body: String? = null) \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/Config.kt b/tangem-core/src/main/java/com/tangem/Config.kt index 2d1193e176..7403a4c771 100644 --- a/tangem-core/src/main/java/com/tangem/Config.kt +++ b/tangem-core/src/main/java/com/tangem/Config.kt @@ -1,6 +1,28 @@ package com.tangem class Config( + /** + * Enables or disables Linked Terminal feature. + + App can optionally generate ECDSA key pair Terminal_PrivateKey / Terminal_PublicKey. + And then submit Terminal_PublicKey to the card in any SIGN command. + Once SIGN is successfully executed by COS (Card Operation System), + including PIN2 verification and/or completion of security delay, the submitted + Terminal_PublicKey key is stored by COS. After that, the App instance is deemed trusted + by COS and COS will allow skipping security delay for subsequent SIGN operations + thus improving convenience without sacrificing security. + + In order to skip security delay, App should use Terminal_PrivateKey to compute the signature + of the data being submitted to SIGN command for signing and transmit this signature in + Terminal_Transaction_Signature parameter in the same SIGN command. COS will verify + the correctness of Terminal_Transaction_Signature using previously stored Terminal_PublicKey + and, if correct, will skip security delay for the current SIGN operation. + */ val linkedTerminal: Boolean = true, + + /** + * If not null, it will be used to validate Issuer data and issuer extra data. + * If null, issuerPublicKey from current card will be used. + */ val issuerPublicKey: ByteArray? = null ) \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/SessionError.kt b/tangem-core/src/main/java/com/tangem/SessionError.kt new file mode 100644 index 0000000000..07209b6a3f --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/SessionError.kt @@ -0,0 +1,139 @@ +package com.tangem + +import com.tangem.commands.Card +import com.tangem.commands.ReadCommand +import com.tangem.common.apdu.StatusWord +import com.tangem.tasks.ScanTask + +/** + * An error class that represent typical errors that may occur when performing Tangem SDK tasks. + * Errors are propagated back to the caller in callbacks. + */ +sealed class SessionError(val code: Int) : Exception() { + + //Errors in serializing APDU + /** + * This error is returned when there [CommandSerializer] cannot deserialize [com.tangem.common.tlv.Tlv] + * (this error is a wrapper around internal [com.tangem.common.tlv.TlvDecoder] errors). + */ + class SerializeCommandError : SessionError(1000) + + class DeserializeApduFailed : SessionError(1001) + class EncodingFailedTypeMismatch : SessionError(1002) + class EncodingFailed : SessionError(1003) + + class DecodingFailedMissingTag : SessionError(1004) + class DecodingFailedTypeMismatch : SessionError(1005) + class DecodingFailed : SessionError(1005) + + /** + * This error is returned when unknown [StatusWord] is received from a card. + */ + class UnknownStatus : SessionError(2001) + + /** + * This error is returned when a card's reply is [StatusWord.ErrorProcessingCommand]. + * The card sends this status in case of internal card error. + */ + class ErrorProcessingCommand : SessionError(2002) + + /** + * This error is returned when a task (such as [ScanTask]) requires that [ReadCommand] + * is executed before performing other commands. + */ + class MissingPreflightRead : SessionError(2003) + + /** + * This error is returned when a card's reply is [StatusWord.InvalidState]. + * The card sends this status when command can not be executed in the current state of a card. + */ + class InvalidState : SessionError(2004) + + /** + * This error is returned when a card's reply is [StatusWord.InsNotSupported]. + * The card sends this status when the card cannot process the [com.tangem.common.apdu.Instruction]. + */ + class InsNotSupported : SessionError(2005) + + /** + * This error is returned when a card's reply is [StatusWord.InvalidParams]. + * The card sends this status when there are wrong or not sufficient parameters in TLV request, + * or wrong PIN1/PIN2. + * The error may be caused, for example, by wrong parameters of the [Task], [CommandSerializer], + * mapping or serialization errors. + */ + class InvalidParams : SessionError(2006) + + /** + * This error is returned when a card's reply is [StatusWord.NeedEncryption] + * and the encryption was not established by TangemSdk. + */ + class NeedEncryption : SessionError(2007) + + //Scan errors + /** + * This error is returned when a [Task] checks unsuccessfully either + * a card's ability to sign with its private key, or the validity of issuer data. + */ + class VerificationFailed : SessionError(3000) + + /** + * This error is returned when a [ScanTask] returns a [Card] without some of the essential fields. + */ + class CardError : SessionError(3001) + + /** + * This error is returned when a [Task] expects a user to use a particular card, + * and a user tries to use a different card. + */ + class WrongCard : SessionError(3002) + + /** + * Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand]. + * This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign. + */ + class TooMuchHashesInOneTransaction : SessionError(3003) + + /** + * This error is returned when a [com.tangem.commands.SignCommand] + * receives only empty hashes for signature. + */ + class EmptyHashes : SessionError(3004) + + /** + * This error is returned when a [com.tangem.commands.SignCommand] + * receives hashes of different lengths for signature. + */ + class HashSizeMustBeEqual : SessionError(3005) + + /** + * This error is returned when [com.tangem.TangemSdk] was called with a new [Task], + * while a previous [Task] is still in progress. + */ + class Busy : SessionError(4000) + + /** + * This error is returned when a user manually closes NFC Reading Bottom Sheet Dialog. + */ + class UserCancelled : SessionError(4001) + + //NFC errors + class NfcReaderError : SessionError(5002) + + /** + * This error is returned when Android NFC reader loses a tag + * (e.g. a user detaches card from the phone's NFC module) while the NFC session is in progress. + */ + class TagLost : SessionError(5003) + + class UnknownError : SessionError(6000) + + //Specific Command Errors + /** + * This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter + * (when the card's requires it), but the counter is missing. + */ + class MissingCounter : SessionError(7001) + + class MissingIssuerPubicKey : SessionError(7002) +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/TangemSdk.kt b/tangem-core/src/main/java/com/tangem/TangemSdk.kt new file mode 100644 index 0000000000..786249d22b --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/TangemSdk.kt @@ -0,0 +1,383 @@ +package com.tangem + +import com.tangem.commands.* +import com.tangem.commands.personalization.DepersonalizeCommand +import com.tangem.commands.personalization.DepersonalizeResponse +import com.tangem.commands.personalization.PersonalizeCommand +import com.tangem.commands.personalization.entities.Acquirer +import com.tangem.commands.personalization.entities.CardConfig +import com.tangem.commands.personalization.entities.Issuer +import com.tangem.commands.personalization.entities.Manufacturer +import com.tangem.common.CompletionResult +import com.tangem.common.TerminalKeysService +import com.tangem.crypto.CryptoUtils +import com.tangem.tasks.CreateWalletTask +import com.tangem.tasks.ScanTask + +/** + * The main interface of Tangem SDK that allows your app to communicate with Tangem cards. + * + * @property reader is an interface that is responsible for NFC connection and + * transfer of data to and from the Tangem Card. + * Its default implementation, NfcCardReader, is in our tangem-sdk module. + * @property viewDelegate An interface that allows interaction with users and shows relevant UI. + * Its default implementation, DefaultCardSessionViewDelegate, is in our tangem-sdk module. + * @property config allows to change a number of parameters for communication with Tangem cards. + * Do not change the default values unless you know what you are doing. + */ +class TangemSdk( + private val reader: CardReader, + private val viewDelegate: CardSessionViewDelegate, + var config: Config = Config() +) { + + private var terminalKeysService: TerminalKeysService? = null + private var cardSession: CardSession? = null + + init { + CryptoUtils.initCrypto() + } + + /** + * This method launches a [ScanTask] on a new thread. + * + * To start using any card, you first need to read it using the scanCard() method. + * This method launches an NFC session, and once it’s connected with the card, + * it obtains the card data. Optionally, if the card contains a wallet (private and public key pair), + * it proves that the wallet owns a private key that corresponds to a public one. + * + * @param callback is triggered on the completion of the [ScanTask] and provides card response + * in the form of [Card] if the task was performed successfully or [SessionError] in case of an error. + */ + fun scanCard(callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate) + cardSession!!.startWithRunnable(ScanTask(), callback) + } + + /** + * This method launches a [SignCommand] on a new thread. + * + * It allows you to sign one or multiple hashes. + * Simultaneous signing of array of hashes in a single [SignCommand] is required to support + * Bitcoin-type multi-input blockchains (UTXO). + * The [SignCommand] will return a corresponding array of signatures. + * + * Please note that Tangem cards usually protect the signing with a security delay + * that may last up to 90 seconds, depending on a card. + * It is for [CardSessionViewDelegate] to notify users of security delay. + * + * @param hashes Array of transaction hashes. It can be from one or up to ten hashes of the same length. + * @param cardId CID, Unique Tangem card ID number + * @param callback is triggered on the completion of the [SignCommand] and provides card response + * in the form of [SignResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun sign(hashes: Array, cardId: String, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(SignCommand(hashes), callback) + } + + /** + * This method launches a [ReadIssuerDataCommand] on a new thread. + + * This command returns 512-byte Issuer Data field and its issuer’s signature. + * Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, + * format and payload of Issuer Data. For example, this field may contain information about + * wallet balance signed by the issuer or additional issuer’s attestation data. + * + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [ReadIssuerDataCommand] and provides + * card response in the form of [ReadIssuerDataResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun readIssuerData(cardId: String, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(ReadIssuerDataCommand(config.issuerPublicKey), callback) + } + + /** + * This method launches a [ReadIssuerExtraDataCommand] on a new thread. + * + * This command retrieves Issuer Extra Data field and its issuer’s signature. + * Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, + * format and payload of Issuer Data. . For example, this field may contain photo or + * biometric information for ID card product. Because of the large size of Issuer_Extra_Data, + * a series of these commands have to be executed to read the entire Issuer_Extra_Data. + * + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [ReadIssuerExtraDataCommand] and provides + * card response in the form of [ReadIssuerExtraDataResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun readIssuerExtraData(cardId: String, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(ReadIssuerExtraDataCommand(config.issuerPublicKey), callback) + } + + /** + * This method launches a [WriteIssuerDataCommand] on a new thread. + * + * This command writes 512-byte Issuer Data field and its issuer’s signature. + * Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, + * format and payload of Issuer Data. For example, this field may contain information about + * wallet balance signed by the issuer or additional issuer’s attestation data. + * + * @param cardId CID, Unique Tangem card ID number. + * @param issuerData Data provided by issuer. + * @param issuerDataSignature Issuer’s signature of [issuerData] with Issuer Data Private Key. + * @param issuerDataCounter An optional counter that protect issuer data against replay attack. + * @param callback is triggered on the completion of the [WriteIssuerDataCommand] and provides + * card response in the form of [WriteIssuerDataResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun writeIssuerData(cardId: String, + issuerData: ByteArray, + issuerDataSignature: ByteArray, + issuerDataCounter: Int? = null, + callback: (result: CompletionResult) -> Unit) { + val command = WriteIssuerDataCommand( + issuerData, + issuerDataSignature, + issuerDataCounter, + config.issuerPublicKey + ) + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(command, callback) + } + + /** + * This method launches a [WriteIssuerExtraDataCommand] on a new thread. + * + * This command writes Issuer Extra Data field and its issuer’s signature. + * Issuer Extra Data is never changed or parsed from within the Tangem COS. + * The issuer defines purpose of use, format and payload of Issuer Data. + * For example, this field may contain a photo or biometric information for ID card products. + * Because of the large size of IssuerExtraData, a series of these commands have to be executed + * to write entire IssuerExtraData. + * + * @param cardId CID, Unique Tangem card ID number. + * @param issuerData Data provided by issuer. + * @param startingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and + * Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData]. + * @param finalizingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay + * andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]). + * @param issuerDataCounter An optional counter that protect issuer data against replay attack. + * @param callback is triggered on the completion of the [WriteIssuerExtraDataCommand] and provides + * card response in the form of [WriteIssuerDataResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun writeIssuerExtraData(cardId: String, + issuerData: ByteArray, + startingSignature: ByteArray, + finalizingSignature: ByteArray, + issuerDataCounter: Int? = null, + callback: (result: CompletionResult) -> Unit) { + val command = WriteIssuerExtraDataCommand( + issuerData, + startingSignature, finalizingSignature, + issuerDataCounter, + config.issuerPublicKey + ) + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(command, callback) + } + + /** + * This method launches a [WriteUserDataCommand] on a new thread. + * + * This command writes some of UserData, UserProtectedData, UserCounter and UserProtectedCounter fields. + * User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS. + * The App defines purpose of use, format and it's payload. For example, this field may contain cashed information + * from blockchain to accelerate preparing new transaction. + * User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing + * of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use. + * For example, this fields may contain blockchain nonce value. + * + * Writing of UserCounter and UserData is protected only by PIN1. + * UserProtectedCounter and UserProtectedData need additionally PIN2 to confirmation. + */ + fun writeUserData( + cardId: String, + userData: ByteArray? = null, + userProtectedData: ByteArray? = null, + userCounter: Int? = null, + userProtectedCounter: Int? = null, + callback: (result: CompletionResult) -> Unit + ) { + val command = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter) + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(command, callback) + } + + /** + * This method launches a [ReadUserDataCommand] on a new thread. + * + * This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and + * User_Protected_Counter fields. + * User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS. + * The App defines purpose of use, format and it's payload. For example, this field may contain cashed information + * from blockchain to accelerate preparing new transaction. + * User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing + * of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use. + * For example, this fields may contain blockchain nonce value. + * + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [ReadUserDataCommand] and provides + * card response in the form of [ReadUserDataResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun readUserData(cardId: String, callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(ReadUserDataCommand(), callback) + } + + /** + * This method launches a [CreateWalletTask] on a new thread. + * + * This this will create a new wallet on the card having ‘Empty’ state with [CreateWalletCommand] + * and will check the success of the operation by performing [CheckWalletCommand]. + * A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card. + * App will need to obtain Wallet_PublicKey from the [CreateWalletResponse] or from the + * response of [ReadCommand] and then transform it into an address of corresponding + * blockchain wallet according to a specific blockchain algorithm. + * WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand]. + * RemainingSignature is set to MaxSignatures. + * + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [CreateWalletTask] and provides + * card response in the form of [CreateWalletResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun createWallet(cardId: String, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(CreateWalletTask(), callback) + } + + /** + * This method launches a [PurgeWalletCommand] on a new thread. + * + * This command deletes all wallet data. If IsReusable flag is enabled during personalization, + + * or [CreateWalletCommand]. + * If IsReusable flag is disabled, the card switches to ‘Purged’ state. + * ‘Purged’ state is final, it makes the card useless. + * + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [PurgeWalletCommand] and provides + * card response in the form of [PurgeWalletResponse] if the task was performed successfully + * or [SessionError] in case of an error. + */ + fun purgeWallet(cardId: String, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(PurgeWalletCommand(), callback) + } + + /** + * Command available on SDK cards only + * + * This method launches a [DepersonalizeCommand] on a new thread. + * + * This command resets card to initial state, + * erasing all data written during personalization and usage. + * + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [DepersonalizeCommand] and provides + * card response in the form of [DepersonalizeResponse] if the task was performed successfully + * or [SessionError] in case of an error. + * */ + fun depersonalize(cardId: String, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId) + cardSession!!.startWithRunnable(DepersonalizeCommand(), callback) + } + + /** + * Command available on SDK cards only + * + * This method launches a [PersonalizeCommand] on a new thread. + * + * Personalization is an initialization procedure, required before starting using a card. + * During this procedure a card setting is set up. + * During this procedure all data exchange is encrypted. + * @param config is a configuration file with all the card settings that are written on the card + * during personalization. + * @param issuer Issuer is a third-party team or company wishing to use Tangem cards. + * @param manufacturer Tangem Card Manufacturer. + * @param acquirer Acquirer is a trusted third-party company that operates proprietary + * (non-EMV) POS terminal infrastructure and transaction processing back-end. + * @param callback is triggered on the completion of the [PersonalizeCommand] and provides + * card response in the form of [Card] if the command was performed successfully + * or [SessionError] in case of an error. + */ + fun personalize(config: CardConfig, + issuer: Issuer, manufacturer: Manufacturer, acquirer: Acquirer? = null, + callback: (result: CompletionResult) -> Unit) { + val command = PersonalizeCommand(config, issuer, manufacturer, acquirer) + cardSession = CardSession(buildEnvironment(), reader, viewDelegate) + cardSession!!.startWithRunnable(command, callback) + } + + /** + * Allows running a custom bunch of commands in one [CardSession] by creating a custom task. + * [TangemSdk] will start a card session, perform preflight [ReadCommand], + * invoke [CardSessionRunnable.run] and close the session. + * You can find the current card in the [CardSession.environment]. + + * @runnable: A custom task, adopting [CardSessionRunnable] protocol + * @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card + * with which you tapped a phone has this [cardId] and SDK will return + * the [SessionError.WrongCard] otherwise. + * @initialMessage: A custom description that shows at the beginning of the NFC session. + * If null, default message will be used. + * @callback: Standard [TangemSdk] callback. + */ + fun startSession( + runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, + callback: (result: CompletionResult) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage) + Thread().run { cardSession!!.startWithRunnable(runnable, callback) } + } + + /** + * Allows running a custom bunch of commands in one [CardSession] with lightweight closure syntax. + * Tangem SDK will start a card sesion and perform preflight [ReadCommand]. + + * @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card + * with which you tapped a phone has this [cardId] and SDK will return + * the [SessionError.WrongCard] otherwise. + * @initialMessage: A custom description that shows at the beginning of the NFC session. + * If null, default message will be used. + * @callback: At first, you should check that the [SessionError] is not null, + * then you can use the [CardSession] to interact with a card. + */ + fun startSession( + cardId: String? = null, initialMessage: Message? = null, + callback: (session: CardSession, error: SessionError?) -> Unit) { + cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage) + Thread().run { cardSession!!.start(callback) } + } + + /** + * Allows to set a particular [TerminalKeysService] to retrieve terminal keys. + * Default implementation is provided in tangem-sdk module: [TerminalKeysStorage]. + */ + fun setTerminalKeysService(terminalKeysService: TerminalKeysService) { + this.terminalKeysService = terminalKeysService + } + + private fun buildEnvironment(): CardEnvironment { + val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null + return CardEnvironment( + terminalKeys = terminalKeys + ) + } + + companion object +} \ No newline at end of file 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 28f0cd2d43..d6df4c4204 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt @@ -1,14 +1,16 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.CardSession +import com.tangem.SessionError +import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag import com.tangem.crypto.CryptoUtils -import com.tangem.tasks.TaskError /** * Deserialized response from the Tangem card after [CheckWalletCommand]. @@ -40,33 +42,54 @@ class CheckWalletResponse( * @property cardId Unique Tangem card ID number * @property challenge Random challenge generated by application */ -class CheckWalletCommand : CommandSerializer() { +class CheckWalletCommand( + private val curve: EllipticCurve, private val publicKey: ByteArray +) : Command() { - val challenge = CryptoUtils.generateRandomBytes(16) + private val challenge = CryptoUtils.generateRandomBytes(16) - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + transceive(session) { result -> + when (result) { + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + is CompletionResult.Success -> { + val verified = result.data.verify( + curve, + publicKey, + challenge + ) + if (verified) { + callback(CompletionResult.Success(result.data)) + } else { + callback(CompletionResult.Failure(SessionError.VerificationFailed())) + } + } + } + } + } + + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.CardId, environment.card?.cardId) tlvBuilder.append(TlvTag.Challenge, challenge) return CommandApdu( Instruction.CheckWallet, tlvBuilder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): CheckWalletResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - CheckWalletResponse( - cardId = mapper.map(TlvTag.CardId), - salt = mapper.map(TlvTag.Salt), - walletSignature = mapper.map(TlvTag.Signature) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + val decoder = TlvDecoder(tlvData) + return CheckWalletResponse( + cardId = decoder.decode(TlvTag.CardId), + salt = decoder.decode(TlvTag.Salt), + walletSignature = decoder.decode(TlvTag.Signature) + ) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/Command.kt b/tangem-core/src/main/java/com/tangem/commands/Command.kt new file mode 100644 index 0000000000..9d06c5fe7f --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/Command.kt @@ -0,0 +1,117 @@ +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.toSessionError +import com.tangem.common.extensions.toInt +import com.tangem.common.tlv.TlvTag + +/** + * Basic interface for a parsed response from [Command]. + */ +interface CommandResponse + +/** + * Basic class for Tangem card commands + */ +abstract class Command : CardSessionRunnable { + + /** + * Serializes data into an array of [com.tangem.common.tlv.Tlv], + * then creates [CommandApdu] with this data. + * @param environment [CardEnvironment] 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: CardEnvironment): 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 [CardEnvironment] of the current card. + * @param apdu received data. + * @return Card response converted to a [CommandResponse] of a type [T] + */ + abstract fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): T + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + transceive(session, callback) + } + + 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 response = deserialize(session.environment, result.data) + callback(CompletionResult.Success(response)) + } + } + } + } catch (error: SessionError) { + callback(CompletionResult.Failure(error)) + } + } + + private fun transceiveApdu(apdu: CommandApdu, session: CardSession, callback: (result: CompletionResult) -> Unit) { + 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.NeedPause -> { + // NeedPause is returned from the card whenever security delay is triggered. + val remainingTime = deserializeSecurityDelay(responseApdu, session.environment) + if (remainingTime != null) { + session.viewDelegate.onSecurityDelay( + 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) + } + else -> { + val error = responseApdu.statusWord.toSessionError() + if (error != null && !tryHandleError(error)) { + callback(CompletionResult.Failure(error)) + } else { + callback(CompletionResult.Failure(SessionError.UnknownError())) + } + } + } + } + is CompletionResult.Failure -> + if (result.error == SessionError.TagLost()) { + session.viewDelegate.onTagLost() + } else if (result.error is SessionError.UserCancelled) { + callback(CompletionResult.Failure(SessionError.UserCancelled())) + session.reader.closeSession() + } + } + } + } + + /** + * Helper method to parse security delay information received from a card. + * + * @return Remaining security delay in milliseconds. + */ + private fun deserializeSecurityDelay(responseApdu: ResponseApdu, cardEnvironment: CardEnvironment): Int? { + val tlv = responseApdu.getTlvData() + return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt() + } + + private fun tryHandleError(error: SessionError) : Boolean { + return false + } + +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt b/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt deleted file mode 100644 index 134474fb23..0000000000 --- a/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.commands - -import com.tangem.common.CardEnvironment -import com.tangem.common.apdu.CommandApdu -import com.tangem.common.apdu.ResponseApdu -import com.tangem.common.extensions.toInt -import com.tangem.common.tlv.TlvTag - -/** - * Simple interface for responses received after sending commands to Tangem cards. - */ -interface CommandResponse - -/** - * Abstract class for all Tangem card commands. - */ -abstract class CommandSerializer { - - /** - * Serializes data into a [List] of [com.tangem.common.tlv.Tlv], - * then creates [CommandApdu] with this data. - * - * @return Command data that can be converted to raw bytes with a method [CommandApdu.toBytes]. - */ - abstract fun serialize(cardEnvironment: CardEnvironment): CommandApdu - - /** - * Deserializes data, received from a card and stored in [ResponseApdu], - * into a [List] of [com.tangem.common.tlv.Tlv]. Then this method maps it into a [CommandResponse]. - * - * @return Card response, converted to a [CommandResponse] of a type [T]. - */ - abstract fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): T? - - /** - * Helper method to parse security delay information received from a card. - * - * @return Remaining security delay in milliseconds. - */ - fun deserializeSecurityDelay(responseApdu: ResponseApdu, cardEnvironment: CardEnvironment): Int? { - val tlv = responseApdu.getTlvData() - return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt() - } -} \ No newline at end of file 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 18b20f1394..d93da64242 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt @@ -1,13 +1,13 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class CreateWalletResponse( /** @@ -35,32 +35,29 @@ class CreateWalletResponse( * * @property cardId CID, Unique Tangem card ID number. */ -class CreateWalletCommand : CommandSerializer() { +class CreateWalletCommand : Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) - tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2) - tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc) + tlvBuilder.append(TlvTag.Pin, environment.pin1) + 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(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): CreateWalletResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - CreateWalletResponse( - cardId = mapper.map(TlvTag.CardId), - status = mapper.map(TlvTag.Status), - walletPublicKey = mapper.map(TlvTag.WalletPublicKey) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + val decoder = TlvDecoder(tlvData) + return CreateWalletResponse( + 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 85d3ddf866..48b3d9a80e 100644 --- a/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt @@ -1,13 +1,13 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class OpenSessionResponse( val sessionKeyB: ByteArray, @@ -20,27 +20,24 @@ class OpenSessionResponse( * to encrypt and decrypt commands’ payload. */ -class OpenSessionCommand(private val sessionKeyA: ByteArray) : CommandSerializer() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { +class OpenSessionCommand(private val sessionKeyA: ByteArray) : Command() { + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA) return CommandApdu( Instruction.OpenSession, tlvBuilder.serialize(), - encryptionMode = cardEnvironment.encryptionMode + encryptionMode = environment.encryptionMode ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): OpenSessionResponse? { - val tlvData = responseApdu.getTlvData() ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): OpenSessionResponse { + val tlvData = apdu.getTlvData() + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - OpenSessionResponse( - sessionKeyB = mapper.map(TlvTag.SessionKeyB), - uid = mapper.map(TlvTag.Uid) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + val decoder = TlvDecoder(tlvData) + return OpenSessionResponse( + sessionKeyB = decoder.decode(TlvTag.SessionKeyB), + uid = decoder.decode(TlvTag.Uid) + ) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt index 4eb379e521..cfdcfd5d04 100644 --- a/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt @@ -1,13 +1,13 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class PurgeWalletResponse( /** @@ -27,29 +27,26 @@ class PurgeWalletResponse( * ‘Purged’ state is final, it makes the card useless. * @property cardId CID, Unique Tangem card ID number. */ -class PurgeWalletCommand : CommandSerializer() { +class PurgeWalletCommand : Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) - tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2) + 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(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): PurgeWalletResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - PurgeWalletResponse( - cardId = mapper.map(TlvTag.CardId), - status = mapper.map(TlvTag.Status)) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + val decoder = TlvDecoder(tlvData) + return PurgeWalletResponse( + cardId = decoder.decode(TlvTag.CardId), + status = decoder.decode(TlvTag.Status)) } } \ No newline at end of file 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 c861d73469..e0deea6cb8 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt @@ -1,14 +1,14 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError 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.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError import java.util.* /** @@ -73,7 +73,7 @@ data class SigningMethod(val rawValue: Int) { if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuerAndWriteIssuerData if (signPos) signingMethod += 0x01 shl SigningMethod.signPos } - return SigningMethod(signingMethod) + return SigningMethod(signingMethod) } } } @@ -354,6 +354,13 @@ class Card( */ val userCounter: Int?, + /** + * This value can be initialized by App (with PIN2 confirmation) and will be increased by COS + * with the execution of each [SignCommand]. For example, this field can store blockchain “nonce” + * for a quick one-touch transaction on POS terminals. Returned only if [SigningMethod.SignPos]. + */ + val userProtectedCounter: Int?, + /** * When this value is true, it means that the application is linked to the card, * and COS will not enforce security delay if [SignCommand] will be called @@ -373,9 +380,9 @@ class Card( * This command receives from the Tangem Card all the data about the card and the wallet, * including unique card number (CID or cardId) that has to be submitted while calling all other commands. */ -class ReadCommand : CommandSerializer() { +class ReadCommand : Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() /** * [CardEnvironment] stores the pin1 value. If no pin1 value was set, it will contain @@ -383,48 +390,46 @@ class ReadCommand : CommandSerializer() { * In order to obtain card’s data, [ReadCommand] should use the correct pin 1 value. * The card will not respond if wrong pin 1 has been submitted. */ - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.TerminalPublicKey, cardEnvironment.terminalKeys?.publicKey) + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey) return CommandApdu( Instruction.Read, tlvBuilder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): Card { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val tlvMapper = TlvMapper(tlvData) + val decoder = TlvDecoder(tlvData) - Card( - cardId = tlvMapper.mapOptional(TlvTag.CardId) ?: "", - manufacturerName = tlvMapper.mapOptional(TlvTag.ManufactureId) ?: "", - status = tlvMapper.mapOptional(TlvTag.Status), + return Card( + cardId = decoder.decodeOptional(TlvTag.CardId) ?: "", + manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "", + status = decoder.decodeOptional(TlvTag.Status), - firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware), - cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey), - settingsMask = tlvMapper.mapOptional(TlvTag.SettingsMask), - issuerPublicKey = tlvMapper.mapOptional(TlvTag.IssuerDataPublicKey), - curve = tlvMapper.mapOptional(TlvTag.CurveId), - maxSignatures = tlvMapper.mapOptional(TlvTag.MaxSignatures), - signingMethod = tlvMapper.mapOptional(TlvTag.SigningMethod), - pauseBeforePin2 = tlvMapper.mapOptional(TlvTag.PauseBeforePin2), - walletPublicKey = tlvMapper.mapOptional(TlvTag.WalletPublicKey), - walletRemainingSignatures = tlvMapper.mapOptional(TlvTag.RemainingSignatures), - walletSignedHashes = tlvMapper.mapOptional(TlvTag.SignedHashes), - health = tlvMapper.mapOptional(TlvTag.Health), - isActivated = tlvMapper.map(TlvTag.IsActivated), - activationSeed = tlvMapper.mapOptional(TlvTag.ActivationSeed), - paymentFlowVersion = tlvMapper.mapOptional(TlvTag.PaymentFlowVersion), - userCounter = tlvMapper.mapOptional(TlvTag.UserCounter), - terminalIsLinked = tlvMapper.map(TlvTag.TerminalIsLinked), + 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), + signingMethod = 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) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + cardData = deserializeCardData(tlvData) + ) } private fun deserializeCardData(tlvData: List): CardData? { @@ -433,18 +438,18 @@ class ReadCommand : CommandSerializer() { } if (cardDataTlvs.isNullOrEmpty()) return null - val tlvMapper = TlvMapper(cardDataTlvs) + val decoder = TlvDecoder(cardDataTlvs) return CardData( - batchId = tlvMapper.mapOptional(TlvTag.Batch), - manufactureDateTime = tlvMapper.mapOptional(TlvTag.ManufactureDateTime), - issuerName = tlvMapper.mapOptional(TlvTag.IssuerId), - blockchainName = tlvMapper.mapOptional(TlvTag.BlockchainId), - manufacturerSignature = tlvMapper.mapOptional(TlvTag.ManufacturerSignature), - productMask = tlvMapper.mapOptional(TlvTag.ProductMask), + 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 = tlvMapper.mapOptional(TlvTag.TokenSymbol), - tokenContractAddress = tlvMapper.mapOptional(TlvTag.TokenContractAddress), - tokenDecimal = tlvMapper.mapOptional(TlvTag.TokenDecimal) + 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/ReadIssuerDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt index cb95fd0899..3909e0cbb6 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt @@ -1,16 +1,19 @@ package com.tangem.commands +import com.tangem.CardEnvironment +import com.tangem.CardSession +import com.tangem.SessionError 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.CardEnvironment +import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class ReadIssuerDataResponse( @@ -51,34 +54,63 @@ class ReadIssuerDataResponse( * @property cardId CID, Unique Tangem card ID number. */ class ReadIssuerDataCommand( + val issuerPublicKey: ByteArray? = null, verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() -) : CommandSerializer(), - IssuerDataVerifier by verifier { +) : Command(), IssuerDataVerifier by verifier { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + val card = session.environment.card + if (card == null) { + callback(CompletionResult.Failure(SessionError.MissingPreflightRead())) + return + } + val publicKey = issuerPublicKey ?: card.issuerPublicKey + if (publicKey == null) { + callback(CompletionResult.Failure(SessionError.MissingIssuerPubicKey())) + return + } + super.run(session) { result -> + when (result) { + is CompletionResult.Failure -> callback(result) + is CompletionResult.Success -> { + if (result.data.issuerData.isEmpty()) { + callback(result) + return@run + } + val issuerDataToVerify = IssuerDataToVerify( + card.cardId, result.data.issuerData, result.data.issuerDataCounter + ) + if (verify(publicKey, result.data.issuerDataSignature, issuerDataToVerify)) { + callback(result) + } else { + callback(CompletionResult.Failure(SessionError.VerificationFailed())) + } + } + } + } + } + + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + 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(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerDataResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): ReadIssuerDataResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - ReadIssuerDataResponse( - cardId = mapper.map(TlvTag.CardId), - issuerData = mapper.map(TlvTag.IssuerData), - issuerDataSignature = mapper.map(TlvTag.IssuerDataSignature), - issuerDataCounter = mapper.mapOptional(TlvTag.IssuerDataCounter) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + 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) + ) } } \ 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 5672003477..83120f9c41 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt @@ -1,16 +1,20 @@ package com.tangem.commands +import com.tangem.CardEnvironment +import com.tangem.CardSession +import com.tangem.SessionError 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.CardEnvironment +import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError +import java.io.ByteArrayOutputStream class ReadIssuerExtraDataResponse( @@ -56,38 +60,113 @@ 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() -) : CommandSerializer(), IssuerDataVerifier by verifier { +) : Command(), IssuerDataVerifier by verifier { - var offset: Int = 0 + private val issuerData = ByteArrayOutputStream() + private var offset: Int = 0 + private var issuerDataSize: Int = 0 - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + val card = session.environment.card + if (card == null) { + callback(CompletionResult.Failure(SessionError.MissingPreflightRead())) + return + } + val publicKey = issuerPublicKey ?: card.issuerPublicKey + if (publicKey == null) { + callback(CompletionResult.Failure(SessionError.MissingIssuerPubicKey())) + return + } + + readIssuerData(session, card.cardId, publicKey, callback) + } + + + private fun readIssuerData( + session: CardSession, + cardId: String, publicKey: ByteArray, + callback: (result: CompletionResult) -> Unit) { + + if (issuerDataSize != 0) { + session.viewDelegate.onDelay( + issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE + ) + } + + transceive(session) { result -> + when (result) { + is CompletionResult.Success -> { + if (result.data.size != null) { + if (result.data.size == 0) { + callback(CompletionResult.Success(result.data)) + return@transceive + } + issuerDataSize = result.data.size + } + issuerData.write(result.data.issuerData) + if (result.data.issuerDataSignature == null) { + offset = issuerData.size() + readIssuerData(session, cardId, publicKey, callback) + } else { + completeTask(result.data, cardId, publicKey, callback) + } + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun completeTask(data: ReadIssuerExtraDataResponse, + cardId: String, publicKey: ByteArray, + callback: (result: CompletionResult) -> Unit) { + val dataToVerify = IssuerDataToVerify( + cardId, + issuerData.toByteArray(), + data.issuerDataCounter + ) + if (verify(publicKey, data.issuerDataSignature!!, dataToVerify)) { + val finalResult = ReadIssuerExtraDataResponse( + data.cardId, + issuerDataSize, + issuerData.toByteArray(), + data.issuerDataSignature, + data.issuerDataCounter + ) + callback(CompletionResult.Success(finalResult)) + } else { + callback(CompletionResult.Failure(SessionError.VerificationFailed())) + } + } + + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.CardId, environment.card?.cardId) tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData) tlvBuilder.append(TlvTag.Offset, offset) return CommandApdu( Instruction.ReadIssuerData, tlvBuilder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerExtraDataResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): ReadIssuerExtraDataResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - ReadIssuerExtraDataResponse( - cardId = mapper.map(TlvTag.CardId), - size = mapper.mapOptional(TlvTag.Size), - issuerData = mapper.mapOptional(TlvTag.IssuerData) ?: byteArrayOf(), - issuerDataSignature = mapper.mapOptional(TlvTag.IssuerDataSignature), - issuerDataCounter = mapper.mapOptional(TlvTag.IssuerDataCounter) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + + 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) + ) } companion object { 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 656c1750f0..7209d424df 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt @@ -1,13 +1,13 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class ReadUserDataResponse( /** @@ -47,33 +47,30 @@ class ReadUserDataResponse( * of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use. * For example, this fields may contain blockchain nonce value. */ -class ReadUserDataCommand: CommandSerializer() { +class ReadUserDataCommand: Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun serialize(environment: CardEnvironment): CommandApdu { val builder = TlvBuilder() - builder.append(TlvTag.CardId, cardEnvironment.cardId) - builder.append(TlvTag.Pin, cardEnvironment.pin1) + builder.append(TlvTag.CardId, environment.card?.cardId) + builder.append(TlvTag.Pin, environment.pin1) return CommandApdu( Instruction.ReadUserData, builder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadUserDataResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): ReadUserDataResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - ReadUserDataResponse( - cardId = mapper.map(TlvTag.CardId), - userData = mapper.map(TlvTag.UserData), - userProtectedData = mapper.map(TlvTag.UserProtectedData), - userCounter = mapper.map(TlvTag.UserCounter), - userProtectedCounter = mapper.map(TlvTag.UserProtectedCounter) + val decoder = TlvDecoder(tlvData) + return ReadUserDataResponse( + cardId = decoder.decode(TlvTag.CardId), + userData = decoder.decode(TlvTag.UserData), + userProtectedData = decoder.decode(TlvTag.UserProtectedData), + userCounter = decoder.decode(TlvTag.UserCounter), + userProtectedCounter = decoder.decode(TlvTag.UserProtectedCounter) ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } } } \ No newline at end of file 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 6f0b7bfacc..0a48b022d6 100644 --- a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt @@ -1,14 +1,14 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag import com.tangem.crypto.sign -import com.tangem.tasks.TaskError /** * @param cardId CID, Unique Tangem card ID number @@ -31,10 +31,26 @@ class SignResponse( * @property cardId CID, Unique Tangem card ID number */ class SignCommand(private val hashes: Array) - : CommandSerializer() { + : Command() { private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0 - private val dataToSign = flattenHashes() + + override fun serialize(environment: CardEnvironment): CommandApdu { + val dataToSign = flattenHashes() + val tlvBuilder = TlvBuilder() + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.Pin2, environment.pin2) + tlvBuilder.append(TlvTag.CardId, environment.card?.cardId) + tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte())) + tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign) + tlvBuilder.append(TlvTag.Cvc, environment.cvc) + + addTerminalSignature(environment, dataToSign, tlvBuilder) + return CommandApdu( + Instruction.Sign, tlvBuilder.serialize(), + environment.encryptionMode, environment.encryptionKey + ) + } private fun flattenHashes(): ByteArray { checkForErrors() @@ -42,25 +58,9 @@ class SignCommand(private val hashes: Array) } private fun checkForErrors() { - if (hashes.isEmpty()) throw TaskError.EmptyHashes() - if (hashes.size > 10) throw TaskError.TooMuchHashesInOneTransaction() - if (hashes.any { it.size != hashSizes }) throw TaskError.HashSizeMustBeEqual() - } - - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { - val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) - tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte())) - tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign) - tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc) - - addTerminalSignature(cardEnvironment, tlvBuilder) - return CommandApdu( - Instruction.Sign, tlvBuilder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey - ) + if (hashes.isEmpty()) throw SessionError.EmptyHashes() + if (hashes.size > 10) throw SessionError.TooMuchHashesInOneTransaction() + if (hashes.any { it.size != hashSizes }) throw SessionError.HashSizeMustBeEqual() } /** @@ -70,23 +70,25 @@ class SignCommand(private val hashes: Array) * TerminalTransactionSignature parameter containing a correct signature of raw data to be signed made with TerminalPrivateKey * (this key should be generated and securily stored by the application). */ - private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvBuilder: TlvBuilder) { - cardEnvironment.terminalKeys?.let { terminalKeyPair -> + private fun addTerminalSignature( + environment: CardEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder) { + environment.terminalKeys?.let { terminalKeyPair -> val signedData = dataToSign.sign(terminalKeyPair.privateKey) tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData) tlvBuilder.append(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey) } } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): SignResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): SignResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - val tlvMapper = TlvMapper(tlvData) + val decoder = TlvDecoder(tlvData) return SignResponse( - cardId = tlvMapper.map(TlvTag.CardId), - signature = tlvMapper.map(TlvTag.Signature), - walletRemainingSignatures = tlvMapper.map(TlvTag.RemainingSignatures), - walletSignedHashes = tlvMapper.map(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 dbc7f15640..0345eb300f 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt @@ -1,16 +1,19 @@ package com.tangem.commands +import com.tangem.CardEnvironment +import com.tangem.CardSession +import com.tangem.SessionError 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.CardEnvironment +import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class WriteIssuerDataResponse( /** @@ -33,13 +36,50 @@ class WriteIssuerDataCommand( private val issuerData: ByteArray, private val issuerDataSignature: ByteArray, private val issuerDataCounter: Int? = null, + private val issuerPublicKey: ByteArray? = null, verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() -) : CommandSerializer(), IssuerDataVerifier by verifier { +) : Command(), IssuerDataVerifier by verifier { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + + val card = session.environment.card + if (card == null) { + callback(CompletionResult.Failure(SessionError.MissingPreflightRead())) + return + } + val publicKey = issuerPublicKey ?: card.issuerPublicKey + if (publicKey == null) { + callback(CompletionResult.Failure(SessionError.MissingIssuerPubicKey())) + return + } + + if (!isCounterValid(issuerDataCounter, card)) { + callback(CompletionResult.Failure(SessionError.MissingCounter())) + } else if (!verifySignature(publicKey, card.cardId)) { + callback(CompletionResult.Failure(SessionError.VerificationFailed())) + } else { + super.run(session, callback) + } + } + + private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean = + if (isCounterRequired(card)) issuerDataCounter != null else true + + private fun isCounterRequired(card: Card): Boolean = + card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false + + private fun verifySignature(publicKey: ByteArray, cardId: String): Boolean { + return verify( + publicKey, + issuerDataSignature, + IssuerDataToVerify(cardId, issuerData, issuerDataCounter) + ) + } + + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.CardId, environment.card?.cardId) tlvBuilder.append(TlvTag.Mode, IssuerDataMode.WriteData) tlvBuilder.append(TlvTag.IssuerData, issuerData) tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature) @@ -47,20 +87,17 @@ class WriteIssuerDataCommand( return CommandApdu( Instruction.WriteIssuerData, tlvBuilder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - WriteIssuerDataResponse( - cardId = mapper.map(TlvTag.CardId) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + val decoder = TlvDecoder(tlvData) + return WriteIssuerDataResponse( + cardId = decoder.decode(TlvTag.CardId) + ) } } \ No newline at end of file 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 753607f0a9..29bf3cd0d5 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt @@ -1,16 +1,19 @@ package com.tangem.commands +import com.tangem.CardEnvironment +import com.tangem.CardSession +import com.tangem.SessionError 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.CardEnvironment +import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError /** * This command writes Issuer Extra Data field and its issuer’s signature. @@ -33,17 +36,94 @@ class WriteIssuerExtraDataCommand( private val startingSignature: ByteArray, private val finalizingSignature: ByteArray, private val issuerDataCounter: Int? = null, + private val issuerPublicKey: ByteArray? = null, verifier: IssuerDataVerifier = DefaultIssuerDataVerifier() -) : CommandSerializer(), IssuerDataVerifier by verifier { +) : Command(), IssuerDataVerifier by verifier { var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData var offset: Int = 0 - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + val card = session.environment.card + if (card == null) { + callback(CompletionResult.Failure(SessionError.MissingPreflightRead())) + return + } + val publicKey = issuerPublicKey ?: card.issuerPublicKey + if (publicKey == null) { + callback(CompletionResult.Failure(SessionError.MissingIssuerPubicKey())) + return + } + + if (!isCounterValid(issuerDataCounter, card)) { + callback(CompletionResult.Failure(SessionError.MissingCounter())) + } else if (!verifySignatures(card.cardId, publicKey)) { + callback(CompletionResult.Failure(SessionError.VerificationFailed())) + } else { + writeIssuerData(session, card.cardId, publicKey, callback) + } + } + + private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean = + if (isCounterRequired(card)) issuerDataCounter != null else true + + private fun isCounterRequired(card: Card): Boolean = + card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false + + private fun verifySignatures(cardId: String, publicKey: ByteArray): Boolean { + + val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size) + val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter) + + return verify(publicKey, startingSignature, firstData) && + verify(publicKey, finalizingSignature, secondData) + } + + private fun writeIssuerData( + session: CardSession, + cardId: String, publicKey: ByteArray, + callback: (result: CompletionResult) -> Unit + ) { + + if (mode == IssuerDataMode.WriteExtraData) { + session.viewDelegate.onDelay(issuerData.size, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE) + } + transceive(session) { result -> + when (result) { + is CompletionResult.Success -> { + when (mode) { + IssuerDataMode.InitializeWritingExtraData -> { + mode = IssuerDataMode.WriteExtraData + writeIssuerData(session, cardId, publicKey, callback) + return@transceive + } + IssuerDataMode.WriteExtraData -> { + offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE + if (offset >= issuerData.size) { + mode = IssuerDataMode.FinalizeExtraData + } + writeIssuerData(session, cardId, publicKey, callback) + return@transceive + } + IssuerDataMode.FinalizeExtraData -> { + callback(CompletionResult.Success(result.data)) + } + } + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + + + } + + override fun serialize(environment: CardEnvironment): CommandApdu { val tlvBuilder = TlvBuilder() - tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) - tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + tlvBuilder.append(TlvTag.Pin, environment.pin1) + tlvBuilder.append(TlvTag.CardId, environment.card?.cardId) tlvBuilder.append(TlvTag.Mode, mode) when (mode) { @@ -62,7 +142,7 @@ class WriteIssuerExtraDataCommand( } return CommandApdu( Instruction.WriteIssuerData, tlvBuilder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey + environment.encryptionMode, environment.encryptionKey ) } @@ -74,17 +154,12 @@ class WriteIssuerExtraDataCommand( return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val mapper = TlvMapper(tlvData) - WriteIssuerDataResponse( - cardId = mapper.map(TlvTag.CardId) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId) + ) } companion object { 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 ac79cba45c..912c57f7e6 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt @@ -1,20 +1,20 @@ package com.tangem.commands -import com.tangem.common.CardEnvironment +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu import com.tangem.common.tlv.TlvBuilder -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag -import com.tangem.tasks.TaskError class WriteUserDataResponse( - /** - * CID, Unique Tangem card ID number. - */ - val cardId: String -): CommandResponse + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String +) : CommandResponse /** * This command write some of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields. @@ -29,33 +29,29 @@ class WriteUserDataResponse( * User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation. */ class WriteUserDataCommand(private val userData: ByteArray? = null, private val userProtectedData: ByteArray? = null, - private val userCounter: Int? = null, - private val userProtectedCounter: Int? = null): CommandSerializer() { + private val userCounter: Int? = null, + private val userProtectedCounter: Int? = null) : Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { - val builder = TlvBuilder() - builder.append(TlvTag.CardId, cardEnvironment.cardId) - builder.append(TlvTag.Pin, cardEnvironment.pin1) - builder.append(TlvTag.UserData, userData) - builder.append(TlvTag.UserCounter, userCounter) - builder.append(TlvTag.UserProtectedData, userProtectedData) - builder.append(TlvTag.UserProtectedCounter, userProtectedCounter) - if (userProtectedCounter != null || userProtectedData != null) - builder.append(TlvTag.Pin2, cardEnvironment.pin2) + override fun serialize(environment: CardEnvironment): CommandApdu { + val builder = TlvBuilder() + builder.append(TlvTag.CardId, environment.card?.cardId) + builder.append(TlvTag.Pin, environment.pin1) + builder.append(TlvTag.UserData, userData) + builder.append(TlvTag.UserCounter, userCounter) + builder.append(TlvTag.UserProtectedData, userProtectedData) + builder.append(TlvTag.UserProtectedCounter, userProtectedCounter) + if (userProtectedCounter != null || userProtectedData != null) + builder.append(TlvTag.Pin2, environment.pin2) - return CommandApdu( - Instruction.WriteUserData, builder.serialize(), - cardEnvironment.encryptionMode, cardEnvironment.encryptionKey - ) - } - - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteUserDataResponse? { - val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null - - return try { - WriteUserDataResponse(TlvMapper(tlvData).map(TlvTag.CardId)) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() + return CommandApdu( + Instruction.WriteUserData, builder.serialize(), + environment.encryptionMode, environment.encryptionKey + ) + } + + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): WriteUserDataResponse { + val tlvData = apdu.getTlvData(environment.encryptionKey) + ?: throw SessionError.DeserializeApduFailed() + return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId)) } - } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt index 034b28dce5..2fcd962966 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt @@ -1,8 +1,8 @@ package com.tangem.commands.personalization +import com.tangem.CardEnvironment +import com.tangem.commands.Command import com.tangem.commands.CommandResponse -import com.tangem.commands.CommandSerializer -import com.tangem.common.CardEnvironment import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -14,17 +14,16 @@ data class DepersonalizeResponse(val success: Boolean) : CommandResponse * * This command resets card to initial state, * erasing all data written during personalization and usage. - * @param cardId CID, Unique Tangem card ID number. */ -class DepersonalizeCommand : CommandSerializer() { +class DepersonalizeCommand : Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun serialize(environment: CardEnvironment): CommandApdu { return CommandApdu( Instruction.Depersonalize, byteArrayOf() ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): DepersonalizeResponse? { + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): DepersonalizeResponse { return DepersonalizeResponse(true) } } \ 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 035963c21a..2d81d479a2 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,10 +1,11 @@ package com.tangem.commands.personalization +import com.tangem.CardEnvironment +import com.tangem.SessionError import com.tangem.commands.Card import com.tangem.commands.CardData -import com.tangem.commands.CommandSerializer +import com.tangem.commands.Command import com.tangem.commands.personalization.entities.* -import com.tangem.common.CardEnvironment import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -12,10 +13,9 @@ 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.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag import com.tangem.crypto.sign -import com.tangem.tasks.TaskError /** * Command available on SDK cards only @@ -34,9 +34,9 @@ class PersonalizeCommand( private val config: CardConfig, private val issuer: Issuer, private val manufacturer: Manufacturer, private val acquirer: Acquirer? = null -) : CommandSerializer() { +) : Command() { - override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + override fun serialize(environment: CardEnvironment): CommandApdu { return CommandApdu( Instruction.Personalize, serializePersonalizationData(config), @@ -44,39 +44,37 @@ class PersonalizeCommand( ) } - override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? { - val tlvData = responseApdu.getTlvData(devPersonalizationKey) ?: return null + override fun deserialize(environment: CardEnvironment, apdu: ResponseApdu): Card { + val tlvData = apdu.getTlvData(devPersonalizationKey) + ?: throw SessionError.DeserializeApduFailed() - return try { - val tlvMapper = TlvMapper(tlvData) - Card( - cardId = tlvMapper.mapOptional(TlvTag.CardId) ?: "", - manufacturerName = tlvMapper.mapOptional(TlvTag.ManufactureId) ?: "", - status = tlvMapper.mapOptional(TlvTag.Status), + val decoder = TlvDecoder(tlvData) + return Card( + cardId = decoder.decodeOptional(TlvTag.CardId) ?: "", + manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "", + status = decoder.decodeOptional(TlvTag.Status), - firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware), - cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey), - settingsMask = tlvMapper.mapOptional(TlvTag.SettingsMask), - issuerPublicKey = tlvMapper.mapOptional(TlvTag.IssuerDataPublicKey), - curve = tlvMapper.mapOptional(TlvTag.CurveId), - maxSignatures = tlvMapper.mapOptional(TlvTag.MaxSignatures), - signingMethod = tlvMapper.mapOptional(TlvTag.SigningMethod), - pauseBeforePin2 = tlvMapper.mapOptional(TlvTag.PauseBeforePin2), - walletPublicKey = tlvMapper.mapOptional(TlvTag.WalletPublicKey), - walletRemainingSignatures = tlvMapper.mapOptional(TlvTag.RemainingSignatures), - walletSignedHashes = tlvMapper.mapOptional(TlvTag.SignedHashes), - health = tlvMapper.mapOptional(TlvTag.Health), - isActivated = tlvMapper.map(TlvTag.IsActivated), - activationSeed = tlvMapper.mapOptional(TlvTag.ActivationSeed), - paymentFlowVersion = tlvMapper.mapOptional(TlvTag.PaymentFlowVersion), - userCounter = tlvMapper.mapOptional(TlvTag.UserCounter), - terminalIsLinked = tlvMapper.map(TlvTag.TerminalIsLinked), + 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), + signingMethod = 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) - ) - } catch (exception: Exception) { - throw TaskError.SerializeCommandError() - } + cardData = deserializeCardData(tlvData) + ) } private fun deserializeCardData(tlvData: List): CardData? { @@ -85,23 +83,23 @@ class PersonalizeCommand( } if (cardDataTlvs.isNullOrEmpty()) return null - val tlvMapper = TlvMapper(cardDataTlvs) + val decoder = TlvDecoder(cardDataTlvs) return CardData( - batchId = tlvMapper.mapOptional(TlvTag.Batch), - manufactureDateTime = tlvMapper.mapOptional(TlvTag.ManufactureDateTime), - issuerName = tlvMapper.mapOptional(TlvTag.IssuerId), - blockchainName = tlvMapper.mapOptional(TlvTag.BlockchainId), - manufacturerSignature = tlvMapper.mapOptional(TlvTag.ManufacturerSignature), - productMask = tlvMapper.mapOptional(TlvTag.ProductMask), + 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 = tlvMapper.mapOptional(TlvTag.TokenSymbol), - tokenContractAddress = tlvMapper.mapOptional(TlvTag.TokenContractAddress), - tokenDecimal = tlvMapper.mapOptional(TlvTag.TokenDecimal) + tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol), + tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress), + tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal) ) } private fun serializePersonalizationData(config: CardConfig): ByteArray { - val cardId = config.createCardId() ?: throw TaskError.SerializeCommandError() + val cardId = config.createCardId() ?: throw SessionError.SerializeCommandError() val tlvBuilder = TlvBuilder() tlvBuilder.append(TlvTag.CardId, cardId) diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Acquirer.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Acquirer.kt index cda239b145..ec4cff04b0 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Acquirer.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Acquirer.kt @@ -1,6 +1,6 @@ package com.tangem.commands.personalization.entities -import com.tangem.common.KeyPair +import com.tangem.KeyPair data class Acquirer( val keyPair: KeyPair, diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Issuer.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Issuer.kt index f82889be6a..ee7ab58fa7 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Issuer.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Issuer.kt @@ -1,6 +1,6 @@ package com.tangem.commands.personalization.entities -import com.tangem.common.KeyPair +import com.tangem.KeyPair data class Issuer( val name: String, diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Manufacturer.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Manufacturer.kt index bffdde9384..9e30287bb0 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Manufacturer.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/entities/Manufacturer.kt @@ -1,6 +1,6 @@ package com.tangem.commands.personalization.entities -import com.tangem.common.KeyPair +import com.tangem.KeyPair data class Manufacturer( val keyPair: KeyPair, diff --git a/tangem-core/src/main/java/com/tangem/common/CompletionResult.kt b/tangem-core/src/main/java/com/tangem/common/CompletionResult.kt index afd76a39e8..71f460c7d7 100644 --- a/tangem-core/src/main/java/com/tangem/common/CompletionResult.kt +++ b/tangem-core/src/main/java/com/tangem/common/CompletionResult.kt @@ -1,7 +1,7 @@ package com.tangem.common +import com.tangem.SessionError import com.tangem.common.CompletionResult.Success -import com.tangem.tasks.TaskError /** * Response class encapsulating successful and failed results. @@ -9,5 +9,5 @@ import com.tangem.tasks.TaskError */ sealed class CompletionResult { class Success(val data: T) : CompletionResult() - class Failure(val error: TaskError) : CompletionResult() + class Failure(val error: SessionError) : CompletionResult() } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/TerminalKeysService.kt b/tangem-core/src/main/java/com/tangem/common/TerminalKeysService.kt index d389c172b3..3ba365247d 100644 --- a/tangem-core/src/main/java/com/tangem/common/TerminalKeysService.kt +++ b/tangem-core/src/main/java/com/tangem/common/TerminalKeysService.kt @@ -1,9 +1,11 @@ package com.tangem.common +import com.tangem.KeyPair + /** * Interface for a service for managing Terminal keypair, used for Linked Terminal feature. - * Its implementation Needs to be provided to [com.tangem.CardManager] - * by calling [com.tangem.CardManager.setTerminalKeysService]. + * Its implementation Needs to be provided to [com.tangem.TangemSdk] + * by calling [com.tangem.TangemSdk.setTerminalKeysService]. * Default implementation is provided in tangem-sdk module: [TerminalKeysStorage]. * Linked Terminal feature can be disabled manually by editing [com.tangem.Config]. */ 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 1a2c2be076..e2d37df4bb 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 @@ -1,6 +1,6 @@ package com.tangem.common.apdu -import com.tangem.common.EncryptionMode +import com.tangem.EncryptionMode import com.tangem.common.extensions.calculateCrc16 import com.tangem.common.extensions.toByteArray import com.tangem.crypto.encrypt diff --git a/tangem-core/src/main/java/com/tangem/common/apdu/StatusWord.kt b/tangem-core/src/main/java/com/tangem/common/apdu/StatusWord.kt index a43cfbe969..3938cee65f 100644 --- a/tangem-core/src/main/java/com/tangem/common/apdu/StatusWord.kt +++ b/tangem-core/src/main/java/com/tangem/common/apdu/StatusWord.kt @@ -1,5 +1,7 @@ package com.tangem.common.apdu +import com.tangem.SessionError + /** * Part of a response from the card, shows the status of the operation */ @@ -21,5 +23,18 @@ enum class StatusWord(val code: Int, val description: String) { private val values = values() fun byCode(code: Int): StatusWord = values.find { it.code == code } ?: Unknown } - +} + +fun StatusWord.toSessionError(): SessionError? { + return when (this) { + StatusWord.ProcessCompleted, StatusWord.Pin1Changed, + StatusWord.Pin2Changed, StatusWord.PinsChanged -> null + StatusWord.NeedPause -> null + StatusWord.InvalidParams -> SessionError.InvalidParams() + StatusWord.ErrorProcessingCommand -> SessionError.ErrorProcessingCommand() + StatusWord.InvalidState -> SessionError.InvalidState() + StatusWord.InsNotSupported -> SessionError.InsNotSupported() + StatusWord.NeedEncryption -> SessionError.NeedEncryption() + StatusWord.Unknown -> SessionError.UnknownStatus() + } } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt similarity index 86% rename from tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt rename to tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt index 1713fd2e2d..3d63f0e27e 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt @@ -1,13 +1,13 @@ package com.tangem.common.tlv import com.tangem.Log +import com.tangem.SessionError import com.tangem.commands.* import com.tangem.commands.common.IssuerDataMode import com.tangem.common.extensions.toDate import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toInt import com.tangem.common.extensions.toUtf8 -import com.tangem.tasks.TaskError import java.util.* /** @@ -16,7 +16,7 @@ import java.util.* * * @property tlvList List of TLVs, which values are to be converted to particular classes. */ -class TlvMapper(val tlvList: List) { +class TlvDecoder(val tlvList: List) { /** * Finds [Tlv] by its [TlvTag]. @@ -26,10 +26,10 @@ class TlvMapper(val tlvList: List) { * * @return Value converted to a nullable type [T]. */ - inline fun mapOptional(tag: TlvTag): T? = + inline fun decodeOptional(tag: TlvTag): T? = try { - map(tag) - } catch (exception: TaskError.MissingTag) { + decode(tag) + } catch (exception: SessionError.DecodingFailedMissingTag) { null } @@ -44,13 +44,13 @@ class TlvMapper(val tlvList: List) { * * @throws [TaskError.MissingTag] exception if no [Tlv] is found by the Tag. */ - inline fun map(tag: TlvTag): T { + inline fun decode(tag: TlvTag): T { val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value ?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) { return false as T } else { Log.e(this::class.simpleName!!, "Tag $tag not found") - throw TaskError.MissingTag() + throw SessionError.DecodingFailedMissingTag() } return when (tag.valueType()) { @@ -68,7 +68,7 @@ class TlvMapper(val tlvList: List) { tlvValue.toInt() as T } catch (exception: IllegalArgumentException) { Log.e(this::class.simpleName!!, exception.message ?: "") - throw TaskError.ConvertError() + throw SessionError.DecodingFailed() } } TlvValueType.BoolValue -> { @@ -85,7 +85,7 @@ class TlvMapper(val tlvList: List) { EllipticCurve.byName(tlvValue.toUtf8()) as T } catch (exception: Exception) { logException(tag, tlvValue.toUtf8(), exception) - throw TaskError.ConvertError() + throw SessionError.DecodingFailed() } @@ -96,7 +96,7 @@ class TlvMapper(val tlvList: List) { tlvValue.toDate() as T } catch (exception: Exception) { logException(tag, tlvValue.toHexString(), exception) - throw TaskError.ConvertError() + throw SessionError.DecodingFailed() } } TlvValueType.ProductMask -> { @@ -113,7 +113,7 @@ class TlvMapper(val tlvList: List) { CardStatus.byCode(tlvValue.toInt()) as T } catch (exception: Exception) { logException(tag, tlvValue.toInt().toString(), exception) - throw TaskError.ConvertError() + throw SessionError.DecodingFailed() } } TlvValueType.SigningMethod -> { @@ -122,7 +122,7 @@ class TlvMapper(val tlvList: List) { SigningMethod(tlvValue.toInt()) as T } catch (exception: Exception) { logException(tag, tlvValue.toInt().toString(), exception) - throw TaskError.ConvertError() + throw SessionError.DecodingFailed() } } TlvValueType.IssuerDataMode -> { @@ -131,7 +131,7 @@ class TlvMapper(val tlvList: List) { IssuerDataMode.byCode(tlvValue.toInt().toByte()) as T } catch (exception: Exception) { logException(tag, tlvValue.toInt().toString(), exception) - throw TaskError.ConvertError() + throw SessionError.DecodingFailed() } } } @@ -146,7 +146,7 @@ class TlvMapper(val tlvList: List) { if (T::class != ExpectedT::class) { Log.e(this::class.simpleName!!, "Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}") - throw TaskError.WrongType() + throw SessionError.DecodingFailedTypeMismatch() } } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt index 58e4043450..1855badcab 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt @@ -1,13 +1,12 @@ package com.tangem.common.tlv import com.tangem.Log +import com.tangem.SessionError import com.tangem.commands.* import com.tangem.commands.common.IssuerDataMode import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toByteArray -import com.tangem.common.extensions.toHexString -import com.tangem.tasks.TaskError import java.util.* /** @@ -25,7 +24,7 @@ class TlvEncoder { return Tlv(tag, encodeValue(tag, value)) } else { Log.e(this::class.simpleName!!, "Encoding error. Value for tag $tag is null") - throw TaskError.SerializeCommandError() + throw SessionError.EncodingFailed() } } @@ -107,7 +106,7 @@ class TlvEncoder { if (T::class != ExpectedT::class) { Log.e(this::class.simpleName!!, "Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}") - throw TaskError.WrongType() + throw SessionError.EncodingFailedTypeMismatch() } } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt b/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt new file mode 100644 index 0000000000..6b21be102c --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt @@ -0,0 +1,43 @@ +package com.tangem.tasks + +import com.tangem.CardSession +import com.tangem.CardSessionRunnable +import com.tangem.SessionError +import com.tangem.commands.CardStatus +import com.tangem.commands.CheckWalletCommand +import com.tangem.commands.CreateWalletCommand +import com.tangem.commands.CreateWalletResponse +import com.tangem.common.CompletionResult + +class CreateWalletTask : CardSessionRunnable { + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + val curve = session.environment.card?.curve + if (curve == null) { + callback(CompletionResult.Failure(SessionError.CardError())) + return + } + + val command = CreateWalletCommand() + command.run(session) { createWalletResult -> + when (createWalletResult) { + is CompletionResult.Failure -> callback(createWalletResult) + is CompletionResult.Success -> { + if (createWalletResult.data.status != CardStatus.Loaded) { + callback(CompletionResult.Failure(SessionError.UnknownError())) + } else { + val checkWalletCommand = CheckWalletCommand( + curve, createWalletResult.data.walletPublicKey + ) + checkWalletCommand.run(session) { result -> + when (result) { + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + is CompletionResult.Success -> callback(createWalletResult) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerDataTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerDataTask.kt deleted file mode 100644 index d7c50dde31..0000000000 --- a/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerDataTask.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tasks - -import com.tangem.commands.Card -import com.tangem.commands.ReadIssuerDataCommand -import com.tangem.commands.ReadIssuerDataResponse -import com.tangem.commands.common.IssuerDataToVerify -import com.tangem.common.CardEnvironment -import com.tangem.common.CompletionResult - -class ReadIssuerDataTask(private val issuerPublicKey: ByteArray? = null) : Task() { - - override fun onRun( - cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit) { - - val command = ReadIssuerDataCommand() - - sendCommand(command, cardEnvironment) { result -> - when (result) { - is CompletionResult.Success -> { - val data = result.data - if (data.issuerData.isEmpty()) { - completeNfcSession() - callback(TaskEvent.Event(result.data)) - callback(TaskEvent.Completion()) - return@sendCommand - } - val publicKey = issuerPublicKey ?: currentCard!!.issuerPublicKey!! - val issuerDataToVerify = IssuerDataToVerify( - cardEnvironment.cardId!!, - data.issuerData, - data.issuerDataCounter - ) - if (command.verify(publicKey, data.issuerDataSignature, issuerDataToVerify)) { - completeNfcSession() - callback(TaskEvent.Event(result.data)) - callback(TaskEvent.Completion()) - } else { - completeNfcSession(TaskError.VerificationFailed()) - callback(TaskEvent.Completion(TaskError.VerificationFailed())) - } - } - is CompletionResult.Failure -> { - if (result.error !is TaskError.UserCancelled) { - completeNfcSession(result.error) - } - callback(TaskEvent.Completion(result.error)) - } - } - } - } -} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt deleted file mode 100644 index 4f1397b8db..0000000000 --- a/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.tasks - -import com.tangem.commands.Card -import com.tangem.commands.ReadIssuerExtraDataCommand -import com.tangem.commands.ReadIssuerExtraDataResponse -import com.tangem.commands.WriteIssuerExtraDataCommand -import com.tangem.commands.common.IssuerDataToVerify -import com.tangem.common.CardEnvironment -import com.tangem.common.CompletionResult -import java.io.ByteArrayOutputStream - -/** - * This task performs [ReadIssuerExtraDataCommand] repeatedly until - * the issuer extra data is fully retrieved. - */ -internal class ReadIssuerExtraDataTask( - private val issuerPublicKey: ByteArray?) : Task() { - - private val issuerData = ByteArrayOutputStream() - private var issuerDataSize = 0 - private lateinit var card: Card - private lateinit var cardEnvironment: CardEnvironment - - override fun onRun( - cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit - ) { - card = currentCard!! - this.cardEnvironment = cardEnvironment - val command = ReadIssuerExtraDataCommand() - readIssuerData(command, callback) - } - - private fun readIssuerData( - command: ReadIssuerExtraDataCommand, - callback: (result: TaskEvent) -> Unit) { - - if (issuerDataSize != 0) { - delegate?.onDelay( - issuerDataSize, command.offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE - ) - } - - sendCommand(command, cardEnvironment) { result -> - when (result) { - - is CompletionResult.Success -> { - if (result.data.size != null) { - if (result.data.size == 0) { - completeNfcSession() - callback(TaskEvent.Event(result.data)) - callback(TaskEvent.Completion()) - return@sendCommand - } - issuerDataSize = result.data.size - } - issuerData.write(result.data.issuerData) - if (result.data.issuerDataSignature == null) { - command.offset = issuerData.size() - readIssuerData(command, callback) - } else { - completeTask(result.data, command, callback) - } - } - is CompletionResult.Failure -> { - if (result.error !is TaskError.UserCancelled) { - completeNfcSession(result.error) - } - callback(TaskEvent.Completion(result.error)) - } - } - } - } - - private fun completeTask(data: ReadIssuerExtraDataResponse, command: ReadIssuerExtraDataCommand, - callback: (result: TaskEvent) -> Unit) { - val publicKey = issuerPublicKey ?: card.issuerPublicKey!! - val dataToVerify = IssuerDataToVerify( - cardEnvironment.cardId!!, - issuerData.toByteArray(), - data.issuerDataCounter - ) - if (command.verify(publicKey, data.issuerDataSignature!!, dataToVerify)) { - completeNfcSession() - val finalResult = ReadIssuerExtraDataResponse( - data.cardId, - issuerDataSize, - issuerData.toByteArray(), - data.issuerDataSignature, - data.issuerDataCounter - ) - callback(TaskEvent.Event(finalResult)) - callback(TaskEvent.Completion()) - } else { - completeNfcSession(TaskError.VerificationFailed()) - callback(TaskEvent.Completion(TaskError.VerificationFailed())) - } - } -} \ No newline at end of file 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 f0160c48b3..207cb6f705 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt @@ -1,78 +1,42 @@ package com.tangem.tasks +import com.tangem.CardSession +import com.tangem.CardSessionRunnable +import com.tangem.SessionError import com.tangem.commands.* -import com.tangem.common.CardEnvironment import com.tangem.common.CompletionResult -/** - * Events that [ScanTask] returns on completion of its commands. - */ -sealed class ScanEvent { - - /** - * Contains data from a Tangem card after successful completion of [ReadCommand]. - */ - data class OnReadEvent(val card: Card) : ScanEvent() - - /** - * Shows whether the Tangem card was verified on completion of [CheckWalletCommand]. - */ - data class OnVerifyEvent(val isGenuine: Boolean) : ScanEvent() -} - /** * Task that allows to read Tangem card and verify its private key. * * It performs two commands, [ReadCommand] and [CheckWalletCommand], subsequently. */ -internal class ScanTask : Task() { +internal class ScanTask : CardSessionRunnable { - override fun onRun(cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit) { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - if (currentCard != null) callback(TaskEvent.Event(ScanEvent.OnReadEvent(currentCard))) + val card = session.environment.card - if (currentCard == null) { - completeNfcSession(TaskError.MissingPreflightRead()) - callback(TaskEvent.Completion(TaskError.MissingPreflightRead())) + if (card == null) { + callback(CompletionResult.Failure(SessionError.MissingPreflightRead())) - } else if (currentCard.cardData?.productMask?.contains(ProductMask.tag) != false) { - completeNfcSession() - callback(TaskEvent.Completion()) + } else if (card.cardData?.productMask?.contains(ProductMask.tag) != false) { + callback(CompletionResult.Success(card)) - } else if (currentCard.status != CardStatus.Loaded) { - completeNfcSession() - callback(TaskEvent.Completion()) + } else if (card.status != CardStatus.Loaded) { + callback(CompletionResult.Success(card)) - } else if (currentCard.curve == null || currentCard.walletPublicKey == null) { - completeNfcSession(TaskError.CardError()) - callback(TaskEvent.Completion(TaskError.CardError())) + } else if (card.curve == null || card.walletPublicKey == null) { + callback(CompletionResult.Failure(SessionError.CardError())) } else { + val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey) - val checkWalletCommand = CheckWalletCommand() - - sendCommand(checkWalletCommand, cardEnvironment) { result -> + checkWalletCommand.run(session) { result -> when (result) { - is CompletionResult.Failure -> { - if (result.error !is TaskError.UserCancelled) { - completeNfcSession(result.error) - } - callback(TaskEvent.Completion(result.error)) - } - is CompletionResult.Success -> { - completeNfcSession() - val verified = result.data.verify( - currentCard.curve, - currentCard.walletPublicKey, - checkWalletCommand.challenge - ) - callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(verified))) - callback(TaskEvent.Completion()) - } + is CompletionResult.Success -> callback(CompletionResult.Success(card)) + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } - } } } diff --git a/tangem-core/src/main/java/com/tangem/tasks/SingleCommandTask.kt b/tangem-core/src/main/java/com/tangem/tasks/SingleCommandTask.kt deleted file mode 100644 index 03efa615f8..0000000000 --- a/tangem-core/src/main/java/com/tangem/tasks/SingleCommandTask.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tasks - -import com.tangem.commands.Card -import com.tangem.common.CardEnvironment -import com.tangem.commands.CommandResponse -import com.tangem.commands.CommandSerializer -import com.tangem.common.CompletionResult - -/** - * Allows to perform a single command. - * - * @property command A command that will be performed. - */ -class SingleCommandTask( - private val command: CommandSerializer -) : Task() { - - override fun onRun( - cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit - ) { - sendCommand(command, cardEnvironment) { result -> - when (result) { - is CompletionResult.Success -> { - completeNfcSession() - callback(TaskEvent.Event(result.data)) - callback(TaskEvent.Completion()) - } - is CompletionResult.Failure -> { - if (result.error !is TaskError.UserCancelled) { - completeNfcSession(result.error) - } - callback(TaskEvent.Completion(result.error)) - } - } - } - } -} diff --git a/tangem-core/src/main/java/com/tangem/tasks/Task.kt b/tangem-core/src/main/java/com/tangem/tasks/Task.kt deleted file mode 100644 index 224b163a9a..0000000000 --- a/tangem-core/src/main/java/com/tangem/tasks/Task.kt +++ /dev/null @@ -1,358 +0,0 @@ -package com.tangem.tasks - -import com.tangem.CardManagerDelegate -import com.tangem.CardReader -import com.tangem.Log -import com.tangem.commands.* -import com.tangem.common.CardEnvironment -import com.tangem.common.CompletionResult -import com.tangem.common.EncryptionMode -import com.tangem.common.apdu.CommandApdu -import com.tangem.common.apdu.StatusWord -import com.tangem.common.extensions.calculateSha256 -import com.tangem.crypto.EncryptionHelper -import com.tangem.crypto.FastEncryptionHelper -import com.tangem.crypto.StrongEncryptionHelper -import com.tangem.crypto.pbkdf2Hash - -/** - * An error class that represent typical errors that may occur when performing Tangem SDK tasks. - * Errors are propagated back to the caller in callbacks. - */ -sealed class TaskError(val code: Int) : Exception() { - - //Errors in serializing APDU - /** - * This error is returned when there [CommandSerializer] cannot deserialize [com.tangem.common.tlv.Tlv] - * (this error is a wrapper around internal [com.tangem.common.tlv.TlvMapper] errors). - */ - class SerializeCommandError : TaskError(1001) - - class EncodingError : TaskError(1002) - class MissingTag : TaskError(1003) - class WrongType : TaskError(1004) - class ConvertError : TaskError(1005) - - /** - * This error is returned when unknown [StatusWord] is received from a card. - */ - class UnknownStatus : TaskError(2001) - - /** - * This error is returned when a card's reply is [StatusWord.ErrorProcessingCommand]. - * The card sends this status in case of internal card error. - */ - class ErrorProcessingCommand : TaskError(2002) - - /** - * This error is returned when a task (such as [ScanTask]) requires that [ReadCommand] - * is executed before performing other commands. - */ - class MissingPreflightRead : TaskError(2003) - - /** - * This error is returned when a card's reply is [StatusWord.InvalidState]. - * The card sends this status when command can not be executed in the current state of a card. - */ - class InvalidState : TaskError(2004) - - /** - * This error is returned when a card's reply is [StatusWord.InsNotSupported]. - * The card sends this status when the card cannot process the [com.tangem.common.apdu.Instruction]. - */ - class InsNotSupported : TaskError(2005) - - /** - * This error is returned when a card's reply is [StatusWord.InvalidParams]. - * The card sends this status when there are wrong or not sufficient parameters in TLV request, - * or wrong PIN1/PIN2. - * The error may be caused, for example, by wrong parameters of the [Task], [CommandSerializer], - * mapping or serialization errors. - */ - class InvalidParams : TaskError(2006) - - /** - * This error is returned when a card's reply is [StatusWord.NeedEncryption] - * and the encryption was not established by TangemSdk. - */ - class NeedEncryption : TaskError(2007) - - //Scan errors - /** - * This error is returned when a [Task] checks unsuccessfully either - * a card's ability to sign with its private key, or the validity of issuer data. - */ - class VerificationFailed : TaskError(3000) - - /** - * This error is returned when a [ScanTask] returns a [Card] without some of the essential fields. - */ - class CardError : TaskError(3001) - - /** - * This error is returned when a [Task] expects a user to use a particular card, - * and a user tries to use a different card. - */ - class WrongCard : TaskError(3002) - - /** - * Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand]. - * This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign. - */ - class TooMuchHashesInOneTransaction : TaskError(3003) - - /** - * This error is returned when a [com.tangem.commands.SignCommand] - * receives only empty hashes for signature. - */ - class EmptyHashes : TaskError(3004) - - /** - * This error is returned when a [com.tangem.commands.SignCommand] - * receives hashes of different lengths for signature. - */ - class HashSizeMustBeEqual : TaskError(3005) - - /** - * This error is returned when [com.tangem.CardManager] was called with a new [Task], - * while a previous [Task] is still in progress. - */ - class Busy : TaskError(4000) - - /** - * This error is returned when a user manually closes NFC Reading Bottom Sheet Dialog. - */ - class UserCancelled : TaskError(4001) - - //NFC errors - class NfcReaderError : TaskError(5002) - - /** - * This error is returned when Android NFC reader loses a tag - * (e.g. a user detaches card from the phone's NFC module) while the NFC session is in progress. - */ - class TagLost : TaskError(5003) - - class UnknownError : TaskError(6000) - - //Specific Command Errors - /** - * This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter - * (when the card's requires it), but the counter is missing. - */ - class MissingCounter : TaskError(7001) -} - -/** - * Events that are are sent in callbacks from [Task]. - */ -sealed class TaskEvent { - - /** - * A callback that is triggered by a Task. - */ - class Event(val data: T) : TaskEvent() - - /** - * A callback that is triggered when a [Task] is completed. - * - * @param error is null if it's a successful completion of a [Task] - */ - class Completion(val error: TaskError? = null) : TaskEvent() -} - -/** - * Allows to perform a group of commands interacting between the card and the application. - * A task opens an NFC session, sends commands to the card and receives its responses, - * repeats the commands if needed, and closes session after receiving the last answer. - */ -abstract class Task { - - var delegate: CardManagerDelegate? = null - var reader: CardReader? = null - var performPreflightRead: Boolean = true - var securityDelayDuration: Int = 0 - - /** - * This method should be called to run the [Task] and perform all its operations. - * - * @param cardEnvironment Relevant current version of a card environment - * @param callback It will be triggered during the performance of the [Task] - */ - fun run(cardEnvironment: CardEnvironment, - callback: (result: TaskEvent) -> Unit) { - delegate?.onNfcSessionStarted(cardEnvironment.cardId) - reader?.openSession() - Log.i(this::class.simpleName!!, "Nfc task is started") - - if (performPreflightRead) { - runWithPreflightRead(cardEnvironment, callback) - } else { - onRun(cardEnvironment, null, callback) - } - } - - /** - * Should be called on [Task] completion, whether it was successful or with failure. - * - * @param taskError The error to be shown by [CardManagerDelegate] - */ - protected fun completeNfcSession(taskError: TaskError? = null) { - reader?.closeSession() - if (taskError != null) { - delegate?.onError(taskError) - } else { - delegate?.onNfcSessionCompleted() - } - } - - /** - * In this method the individual Tasks' logic should be implemented. - */ - protected abstract fun onRun(cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit) - - /** - * This method should be called by Tasks in their [onRun] method wherever - * they need to communicate with the Tangem Card by launching commands. - */ - protected fun sendCommand( - command: CommandSerializer, - cardEnvironment: CardEnvironment, - callback: (result: CompletionResult) -> Unit) { - - Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} is initiated") - - when (cardEnvironment.encryptionMode) { - EncryptionMode.NONE -> { - val commandApdu = command.serialize(cardEnvironment) - sendRequest(command, commandApdu, cardEnvironment, callback) - } - EncryptionMode.FAST, EncryptionMode.STRONG -> { - if (cardEnvironment.encryptionKey != null ) { - val commandApdu = command.serialize(cardEnvironment) - sendRequest(command, commandApdu, cardEnvironment, callback) - return - } - val encryptionHelper: EncryptionHelper = - if (cardEnvironment.encryptionMode == EncryptionMode.STRONG) { - StrongEncryptionHelper() - } else { - FastEncryptionHelper() - } - val openSessionCommand = OpenSessionCommand(encryptionHelper.keyA) - val openSessionApdu = openSessionCommand.serialize(cardEnvironment) - sendRequest(openSessionCommand, openSessionApdu, cardEnvironment) { result -> - when (result) { - is CompletionResult.Success -> { - val uid = result.data.uid - val protocolKey = cardEnvironment.pin1.calculateSha256().pbkdf2Hash(uid, 50) - val secret = encryptionHelper.generateSecret(result.data.sessionKeyB) - val sessionKey = (secret + protocolKey).calculateSha256() - cardEnvironment.encryptionKey = sessionKey - - sendCommand(command, cardEnvironment, callback) - } - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) - } - } - } - } - } - - private fun sendRequest(command: CommandSerializer, - commandApdu: CommandApdu, - cardEnvironment: CardEnvironment, - callback: (result: CompletionResult) -> Unit) { - - reader?.transceiveApdu(commandApdu) { result -> - - when (result) { - is CompletionResult.Success -> { - val responseApdu = result.data - when (responseApdu.statusWord) { - StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged - -> { - try { - val responseData = command.deserialize(cardEnvironment, responseApdu) - Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} is completed") - callback(CompletionResult.Success(responseData as T)) - } catch (error: TaskError) { - callback(CompletionResult.Failure(error)) - } - } - StatusWord.InvalidParams -> callback(CompletionResult.Failure(TaskError.InvalidParams())) - StatusWord.Unknown -> { - Log.e(this::class.simpleName!!, "Unknown status error: ${result.data.sw}") - callback(CompletionResult.Failure(TaskError.UnknownStatus())) - } - StatusWord.ErrorProcessingCommand -> callback(CompletionResult.Failure(TaskError.ErrorProcessingCommand())) - StatusWord.InvalidState -> callback(CompletionResult.Failure(TaskError.InvalidState())) - - StatusWord.InsNotSupported -> callback(CompletionResult.Failure(TaskError.InsNotSupported())) - StatusWord.NeedEncryption -> { - when (cardEnvironment.encryptionMode) { - EncryptionMode.NONE -> { - cardEnvironment.encryptionKey = null - cardEnvironment.encryptionMode = EncryptionMode.FAST - } - EncryptionMode.FAST -> { - cardEnvironment.encryptionKey = null - cardEnvironment.encryptionMode = EncryptionMode.STRONG - } - EncryptionMode.STRONG -> { - Log.e(this::class.simpleName!!, "Encryption doesn't work") - callback(CompletionResult.Failure(TaskError.NeedEncryption())) - return@transceiveApdu - } - } - sendCommand(command, cardEnvironment, callback) - } - StatusWord.NeedPause -> { - // NeedPause is returned from the card whenever security delay is triggered. - val remainingTime = command.deserializeSecurityDelay(responseApdu, cardEnvironment) - if (remainingTime != null) delegate?.onSecurityDelay(remainingTime, securityDelayDuration) - Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} triggered security delay of $remainingTime milliseconds") - sendRequest(command, commandApdu, cardEnvironment, callback) - } - } - } - is CompletionResult.Failure -> - if (result.error == TaskError.TagLost()) { - delegate?.onTagLost() - } else if (result.error is TaskError.UserCancelled) { - callback(CompletionResult.Failure(TaskError.UserCancelled())) - reader?.closeSession() - } - } - } - } - - private fun runWithPreflightRead( - environment: CardEnvironment, callback: (result: TaskEvent) -> Unit) { - sendCommand(ReadCommand(), environment) { readResult -> - when (readResult) { - is CompletionResult.Failure -> { - completeNfcSession(readResult.error) - callback(TaskEvent.Completion(readResult.error)) - } - is CompletionResult.Success -> { - val receivedCardId = readResult.data.cardId - securityDelayDuration = readResult.data.pauseBeforePin2 ?: 0 - - if (environment.cardId != null && environment.cardId != receivedCardId) { - completeNfcSession(TaskError.WrongCard()) - callback(TaskEvent.Completion(TaskError.WrongCard())) - return@sendCommand - } - - val newEnvironment = environment.copy(cardId = receivedCardId) - onRun(newEnvironment, readResult.data, callback) - } - } - } - } -} - - diff --git a/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerDataTask.kt b/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerDataTask.kt deleted file mode 100644 index b22ed95049..0000000000 --- a/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerDataTask.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.tasks - -import com.tangem.commands.Card -import com.tangem.commands.Settings -import com.tangem.commands.WriteIssuerDataCommand -import com.tangem.commands.WriteIssuerDataResponse -import com.tangem.commands.common.IssuerDataToVerify -import com.tangem.common.CardEnvironment -import com.tangem.common.CompletionResult - -class WriteIssuerDataTask( - private val issuerData: ByteArray, - private val issuerDataSignature: ByteArray, - private val issuerDataCounter: Int? = null, - private val issuerPublicKey: ByteArray? = null -) : Task() { - - private lateinit var card: Card - - override fun onRun( - cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit) { - - card = currentCard!! - val command = WriteIssuerDataCommand( - issuerData, issuerDataSignature, issuerDataCounter - ) - if (!isCounterValid(issuerDataCounter)) { - completeNfcSession(TaskError.MissingCounter()) - callback(TaskEvent.Completion(TaskError.MissingCounter())) - } else if (!verifySignature(command, cardEnvironment.cardId!!)) { - completeNfcSession(TaskError.VerificationFailed()) - callback(TaskEvent.Completion(TaskError.VerificationFailed())) - } - - sendCommand(command, cardEnvironment) { result -> - when (result) { - is CompletionResult.Success -> { - completeNfcSession() - callback(TaskEvent.Event(result.data)) - callback(TaskEvent.Completion()) - } - is CompletionResult.Failure -> { - if (result.error !is TaskError.UserCancelled) { - completeNfcSession(result.error) - } - callback(TaskEvent.Completion(result.error)) - } - } - } - } - - private fun isCounterValid(issuerDataCounter: Int?): Boolean = - if (isCounterRequired()) issuerDataCounter != null else true - - private fun isCounterRequired(): Boolean = - card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false - - private fun verifySignature(command: WriteIssuerDataCommand, cardId: String): Boolean { - return command.verify( - issuerPublicKey ?: card.issuerPublicKey!!, - issuerDataSignature, - IssuerDataToVerify(cardId, issuerData, issuerDataCounter) - ) - } -} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt b/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt deleted file mode 100644 index 887e832548..0000000000 --- a/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.tasks - -import com.tangem.commands.* -import com.tangem.commands.common.IssuerDataMode -import com.tangem.commands.common.IssuerDataToVerify -import com.tangem.common.CardEnvironment -import com.tangem.common.CompletionResult - -/** - * This task performs [WriteIssuerExtraDataCommand] repeatedly until the issuer extra data is fully - * written on the card. - * @param issuerData Data provided by issuer. - * @param startingSignature Issuer’s signature with Issuer Data Private Key of [cardId], - * [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and - * Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData]. - * @param finalizingSignature Issuer’s signature with Issuer Data Private Key of [cardId], - * [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay - * andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]). - * @param issuerDataCounter An optional counter that protect issuer data against replay attack. - */ -internal class WriteIssuerExtraDataTask( - private val issuerData: ByteArray, - private val startingSignature: ByteArray, - private val finalizingSignature: ByteArray, - private val issuerPublicKey: ByteArray? = null, - private val issuerDataCounter: Int? = null -) : Task() { - - private lateinit var card: Card - private lateinit var cardEnvironment: CardEnvironment - - override fun onRun(cardEnvironment: CardEnvironment, - currentCard: Card?, - callback: (result: TaskEvent) -> Unit) { - - card = currentCard!! - this.cardEnvironment = cardEnvironment - val command = WriteIssuerExtraDataCommand( - issuerData, startingSignature, finalizingSignature, issuerDataCounter - ) - if (!isCounterValid(issuerDataCounter)) { - completeNfcSession(TaskError.MissingCounter()) - callback(TaskEvent.Completion(TaskError.MissingCounter())) - } else if (!verifySignatures(command)) { - completeNfcSession(TaskError.VerificationFailed()) - callback(TaskEvent.Completion(TaskError.VerificationFailed())) - } - - writeIssuerData(command, callback) - } - - private fun writeIssuerData( - command: WriteIssuerExtraDataCommand, - callback: (result: TaskEvent) -> Unit) { - - if (command.mode == IssuerDataMode.WriteExtraData) { - delegate?.onDelay(issuerData.size, command.offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE) - } - sendCommand(command, cardEnvironment) { result -> - when (result) { - - is CompletionResult.Success -> { - when (command.mode) { - IssuerDataMode.InitializeWritingExtraData -> { - command.mode = IssuerDataMode.WriteExtraData - writeIssuerData(command, callback) - return@sendCommand - } - IssuerDataMode.WriteExtraData -> { - command.offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE - if (command.offset >= issuerData.size) { - command.mode = IssuerDataMode.FinalizeExtraData - } - writeIssuerData(command, callback) - return@sendCommand - } - IssuerDataMode.FinalizeExtraData -> { - completeNfcSession() - callback(TaskEvent.Event(result.data)) - callback(TaskEvent.Completion()) - } - } - } - is CompletionResult.Failure -> { - if (result.error !is TaskError.UserCancelled) { - completeNfcSession(result.error) - } - callback(TaskEvent.Completion(result.error)) - } - } - } - } - - private fun isCounterValid(issuerDataCounter: Int?): Boolean = - if (isCounterRequired()) issuerDataCounter != null else true - - private fun isCounterRequired(): Boolean = - card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false - - private fun verifySignatures(command: WriteIssuerExtraDataCommand): Boolean { - val publicKey = issuerPublicKey ?: card.issuerPublicKey!! - val cardId = cardEnvironment.cardId!! - - val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size) - val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter) - - return command.verify(publicKey, startingSignature, firstData) && - command.verify(publicKey, finalizingSignature, secondData) - } -} \ No newline at end of file diff --git a/tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt b/tangem-core/src/test/java/com/tangem/common/tlv/TlvDecoderTest.kt similarity index 71% rename from tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt rename to tangem-core/src/test/java/com/tangem/common/tlv/TlvDecoderTest.kt index 64cb5ce15d..f9915fc7b8 100644 --- a/tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt +++ b/tangem-core/src/test/java/com/tangem/common/tlv/TlvDecoderTest.kt @@ -1,69 +1,69 @@ package com.tangem.common.tlv import com.google.common.truth.Truth.assertThat +import com.tangem.SessionError import com.tangem.commands.* import com.tangem.common.extensions.hexToBytes -import com.tangem.tasks.TaskError import org.junit.Test import org.junit.jupiter.api.assertThrows import java.util.* -class TlvMapperTest { +class TlvDecoderTest { private val rawData = byteArrayOf(1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 32, 11, 83, 77, 65, 82, 84, 32, 67, 65, 83, 72, 0, 2, 1, 2, -128, 6, 50, 46, 49, 49, 114, 0, 3, 65, 4, -49, 11, -50, -66, -121, -25, -2, 65, 65, -13, 14, 49, 27, -82, -33, -85, -113, 65, 20, 8, -39, -75, 57, 45, 65, -31, 35, 44, 38, 40, 63, -44, 113, -45, -75, -95, -118, 118, 29, 65, 117, -24, -53, 82, -72, 91, -20, -96, -77, -103, -14, -63, 52, -127, -123, -27, -16, -128, -67, -3, -104, -26, -22, 65, 10, 4, 0, 0, 126, 33, 12, 90, -127, 2, 0, 41, -126, 4, 7, -29, 5, 2, -125, 7, 84, 65, 78, 71, 69, 77, 0, -124, 3, 69, 84, 72, -122, 64, 111, -103, 48, -114, -40, 18, -103, 26, -102, -12, -38, -78, -90, -9, -98, 88, -47, -100, -24, 24, -105, -70, -72, 6, 94, -96, -77, 11, -123, -28, -118, 37, 63, 107, -55, -11, 23, -12, 13, -23, -121, -63, 36, -59, 70, 116, 91, -125, -34, -69, 23, -112, 6, 17, 4, -49, 68, -56, 29, -45, 81, 10, 97, 83, 48, 65, 4, -127, -106, -86, 75, 65, 10, -60, 74, 59, -100, -50, 24, -25, -66, 34, 106, -22, 7, 10, -52, -125, -87, -49, 103, 84, 15, -84, 73, -81, 37, 18, -97, 106, 83, -118, 40, -83, 99, 65, 53, -114, 60, 79, -103, 99, 6, 79, 126, 54, 83, 114, -90, 81, -45, 116, -27, -62, 60, -35, 55, -3, 9, -101, -14, 5, 10, 115, 101, 99, 112, 50, 53, 54, 107, 49, 0, 8, 4, 0, 15, 66, 64, 7, 1, 0, 9, 2, 11, -72, 96, 65, 4, -42, -5, -41, -84, -23, 88, 2, 86, -63, -118, -123, -10, -66, -82, -107, -68, -93, 111, 47, 93, -20, -86, 74, 28, 21, 81, 93, -21, -124, -57, -102, 55, 17, 84, -66, -68, -22, -128, 126, -99, -65, -54, -42, 59, -25, -21, -124, 5, 59, -16, -72, 73, 48, 16, -27, 103, -112, -73, 2, 96, -51, 41, -42, 116, 98, 4, 0, 15, 66, 52, 99, 4, 0, 0, 0, 13, 15, 1, 0) private val tlvData = Tlv.deserialize(rawData) - private val tlvMapper = TlvMapper(tlvData!!) + private val tlvMapper = TlvDecoder(tlvData!!) - private val cardDataRaw: ByteArray = tlvMapper.map(TlvTag.CardData) - private val cardDataMapper = TlvMapper(Tlv.deserialize(cardDataRaw)!!) + private val cardDataRaw: ByteArray = tlvMapper.decode(TlvTag.CardData) + private val cardDataMapper = TlvDecoder(Tlv.deserialize(cardDataRaw)!!) @Test fun `map optional when value is present`() { - val settingsMask: SettingsMask? = tlvMapper.mapOptional(TlvTag.SettingsMask) + val settingsMask: SettingsMask? = tlvMapper.decodeOptional(TlvTag.SettingsMask) assertThat(settingsMask) .isNotNull() } @Test fun `map optional when no tag returns null`() { - val tokenSymbol: String? = tlvMapper.mapOptional(TlvTag.TokenSymbol) + val tokenSymbol: String? = tlvMapper.decodeOptional(TlvTag.TokenSymbol) assertThat(tokenSymbol) .isNull() } @Test fun `map when value is null throws MissingTagException`() { - assertThrows { - tlvMapper.map(TlvTag.TokenSymbol) + assertThrows { + tlvMapper.decode(TlvTag.TokenSymbol) } } @Test fun `map optional to wrong type throws WrongTypeException`() { - assertThrows { - tlvMapper.mapOptional(TlvTag.CardData) + assertThrows { + tlvMapper.decodeOptional(TlvTag.CardData) } } @Test fun `map to wrong type throws WrongTypeException`() { - assertThrows { - tlvMapper.map(TlvTag.CardData) + assertThrows { + tlvMapper.decode(TlvTag.CardData) } } @Test fun `map boolean missing flag returns false`() { - val terminalIsLinked: Boolean = tlvMapper.map(TlvTag.TerminalIsLinked) + val terminalIsLinked: Boolean = tlvMapper.decode(TlvTag.TerminalIsLinked) assertThat(terminalIsLinked) .isFalse() } @Test fun `map SettingsMask returns correct value`() { - val settingsMask: SettingsMask = tlvMapper.map(TlvTag.SettingsMask) + val settingsMask: SettingsMask = tlvMapper.decode(TlvTag.SettingsMask) assertThat(settingsMask) .isNotNull() assertThat(settingsMask.rawValue) @@ -82,16 +82,16 @@ class TlvMapperTest { @Test fun `map SigningMethods single value returns correct value`() { - val signingMethods: SigningMethod = tlvMapper.map(TlvTag.SigningMethod) + val signingMethods: SigningMethod = tlvMapper.decode(TlvTag.SigningMethod) assertThat(signingMethods.contains(SigningMethod.signHash)) .isTrue() } @Test fun `map SigningMethods set of methods returns correct value`() { - val localMapper = TlvMapper(Tlv.deserialize("070195".hexToBytes())!!) + val localMapper = TlvDecoder(Tlv.deserialize("070195".hexToBytes())!!) - val signingMethod: SigningMethod = localMapper.map(TlvTag.SigningMethod) + val signingMethod: SigningMethod = localMapper.decode(TlvTag.SigningMethod) assertThat(signingMethod.contains(SigningMethod.signHash)) .isTrue() assertThat(signingMethod.contains(SigningMethod.signHashValidatedByIssuer)) @@ -110,38 +110,38 @@ class TlvMapperTest { @Test fun `map CardStatus returns correct value`() { - val cardStatus: CardStatus = tlvMapper.map(TlvTag.Status) + val cardStatus: CardStatus = tlvMapper.decode(TlvTag.Status) assertThat(cardStatus) .isEqualTo(CardStatus.Loaded) } @Test fun `map ProductMask with raw value 5 returns correct value`() { - val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5)))) - val productMask: ProductMask = localMapper.map(TlvTag.ProductMask) + val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5)))) + val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask) assertThat(productMask.contains(ProductMask.note) && productMask.contains(ProductMask.idCard)) .isTrue() } @Test fun `map ProductMask with raw value 1 returns correct value`() { - val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(1)))) - val productMask: ProductMask = localMapper.map(TlvTag.ProductMask) + val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(1)))) + val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask) assertThat(productMask.contains(ProductMask.note)) .isTrue() } @Test fun `map Enum with unknown code throws ConversionException error`() { - val localMapper = TlvMapper(listOf(Tlv(TlvTag.CurveId, "test".toByteArray()))) - assertThrows { - localMapper.map(TlvTag.CurveId) + val localMapper = TlvDecoder(listOf(Tlv(TlvTag.CurveId, "test".toByteArray()))) + assertThrows { + localMapper.decode(TlvTag.CurveId) } } @Test fun `map DateTime returns correct value`() { - val date: Date = cardDataMapper.map(TlvTag.ManufactureDateTime) + val date: Date = cardDataMapper.decode(TlvTag.ManufactureDateTime) val expected = Calendar.getInstance().apply { this.set(2019, 4, 2, 0, 0, 0) }.time assertThat(date.toString()) .isEqualTo(expected.toString()) @@ -149,43 +149,43 @@ class TlvMapperTest { @Test fun `map EllipticCurve returns correct value`() { - val ellipticCurve: EllipticCurve = tlvMapper.map(TlvTag.CurveId) + val ellipticCurve: EllipticCurve = tlvMapper.decode(TlvTag.CurveId) assertThat(ellipticCurve) .isEqualTo(EllipticCurve.Secp256k1) } @Test fun `map ByteArray returns correctly`() { - val cardPublicKey: ByteArray = tlvMapper.map(TlvTag.CardPublicKey) + val cardPublicKey: ByteArray = tlvMapper.decode(TlvTag.CardPublicKey) assertThat(cardPublicKey) .isInstanceOf(ByteArray::class.java) } @Test fun `map Int returns correct value`() { - val signedHashes: Int = tlvMapper.map(TlvTag.SignedHashes) + val signedHashes: Int = tlvMapper.decode(TlvTag.SignedHashes) assertThat(signedHashes) .isEqualTo(13) } @Test fun `map Int with wrong value throws ConversionException`() { - val localMapper = TlvMapper(listOf(Tlv(TlvTag.SignedHashes, byteArrayOf(1, 2, 3, 4, 5)))) - assertThrows { - localMapper.map(TlvTag.SignedHashes) + val localMapper = TlvDecoder(listOf(Tlv(TlvTag.SignedHashes, byteArrayOf(1, 2, 3, 4, 5)))) + assertThrows { + localMapper.decode(TlvTag.SignedHashes) } } @Test fun `map UTF8 returns correct value`() { - val blockchainId: String = cardDataMapper.map(TlvTag.BlockchainId) + val blockchainId: String = cardDataMapper.decode(TlvTag.BlockchainId) assertThat(blockchainId) .isEqualTo("ETH") } @Test fun `map Hex returns correct value`() { - val cardId: String = tlvMapper.map(TlvTag.CardId) + val cardId: String = tlvMapper.decode(TlvTag.CardId) assertThat(cardId) .isEqualTo("cb22000000027374") } diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultCardManagerDelegate.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultCardSessionViewDelegate.kt similarity index 87% rename from tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultCardManagerDelegate.kt rename to tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultCardSessionViewDelegate.kt index e1dd5bb69b..56cce78f88 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultCardManagerDelegate.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultCardSessionViewDelegate.kt @@ -6,24 +6,24 @@ import android.view.View import android.view.animation.DecelerateInterpolator import androidx.fragment.app.FragmentActivity import com.google.android.material.bottomsheet.BottomSheetDialog -import com.tangem.CardManagerDelegate +import com.tangem.CardSessionViewDelegate import com.tangem.Log import com.tangem.LoggerInterface +import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.tangem_sdk_new.extensions.hide import com.tangem.tangem_sdk_new.extensions.show import com.tangem.tangem_sdk_new.nfc.NfcReader import com.tangem.tangem_sdk_new.ui.NfcEnableDialog import com.tangem.tangem_sdk_new.ui.TouchCardAnimation -import com.tangem.tasks.TaskError import kotlinx.android.synthetic.main.layout_touch_card.* import kotlinx.android.synthetic.main.nfc_bottom_sheet.* /** - * Default implementation of [CardManagerDelegate]. + * Default implementation of [CardSessionViewDelegate]. * If no customisation is required, this is the preferred way to use Tangem SDK. */ -class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDelegate { +class DefaultCardSessionViewDelegate(private val reader: NfcReader) : CardSessionViewDelegate { lateinit var activity: FragmentActivity private var readingDialog: BottomSheetDialog? = null @@ -33,13 +33,13 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel setLogger() } - override fun onNfcSessionStarted(cardId: String?) { + override fun onNfcSessionStarted(cardId: String?, message: Message?) { reader.readingCancelled = false - postUI { showReadingDialog(activity, cardId) } + postUI { showReadingDialog(activity, cardId, message) } if (!reader.nfcEnabled) showNFCEnableDialog() } - private fun showReadingDialog(activity: FragmentActivity, cardId: String?) { + private fun showReadingDialog(activity: FragmentActivity, cardId: String?, message: Message?) { val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null) readingDialog = BottomSheetDialog(activity) readingDialog?.setContentView(dialogView) @@ -56,6 +56,10 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel 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 @@ -132,25 +136,29 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel } } - override fun onNfcSessionCompleted() { + 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 onError(error: TaskError) { + override fun onError(errorMessage: String) { 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 = "${error::class.simpleName}: ${error.code}" + readingDialog?.tvTaskText?.text = errorMessage performHapticFeedback() } } diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/TerminalKeysStorage.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/TerminalKeysStorage.kt index 70b2cfa431..0e4eabae9c 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/TerminalKeysStorage.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/TerminalKeysStorage.kt @@ -3,7 +3,7 @@ package com.tangem.tangem_sdk_new import android.app.Application import android.content.SharedPreferences import at.favre.lib.armadillo.Armadillo -import com.tangem.common.KeyPair +import com.tangem.KeyPair import com.tangem.common.TerminalKeysService import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString @@ -12,7 +12,7 @@ import com.tangem.crypto.CryptoUtils /** * Service for managing Terminal keypair, used for Linked Terminal feature. - * Needs to be provided to [com.tangem.CardManager] by calling [com.tangem.CardManager.setTerminalKeysService] + * Needs to be provided to [com.tangem.TangemSdk] by calling [com.tangem.TangemSdk.setTerminalKeysService] * Linked Terminal feature can be disabled manually by editing [com.tangem.Config]. * @param applicationContext is required to retrieve an instance of [SharedPreferences] */ diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/CardManager.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdk.kt similarity index 62% rename from tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/CardManager.kt rename to tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdk.kt index b2364c13cf..90a47a9d29 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/CardManager.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdk.kt @@ -1,21 +1,21 @@ package com.tangem.tangem_sdk_new.extensions import androidx.fragment.app.FragmentActivity -import com.tangem.CardManager -import com.tangem.tangem_sdk_new.DefaultCardManagerDelegate +import com.tangem.TangemSdk +import com.tangem.tangem_sdk_new.DefaultCardSessionViewDelegate import com.tangem.tangem_sdk_new.NfcLifecycleObserver import com.tangem.tangem_sdk_new.TerminalKeysStorage import com.tangem.tangem_sdk_new.nfc.NfcManager -fun CardManager.Companion.init(activity: FragmentActivity): CardManager { +fun TangemSdk.Companion.init(activity: FragmentActivity): TangemSdk { val nfcManager = NfcManager().apply { this.setCurrentActivity(activity) activity.lifecycle.addObserver(NfcLifecycleObserver(this)) } - val cardManagerDelegate = DefaultCardManagerDelegate(nfcManager.reader).apply { + val viewDelegate = DefaultCardSessionViewDelegate(nfcManager.reader).apply { this.activity = activity } - return CardManager(nfcManager.reader, cardManagerDelegate).apply { + return TangemSdk(nfcManager.reader, viewDelegate).apply { this.setTerminalKeysService(TerminalKeysStorage(activity.application)) } } \ No newline at end of file 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 5ba6b11d4b..845dbb2abd 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 @@ -6,10 +6,10 @@ import android.nfc.tech.IsoDep import android.nfc.tech.NfcV import com.tangem.CardReader import com.tangem.Log +import com.tangem.SessionError import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.ResponseApdu -import com.tangem.tasks.TaskError /** * Provides NFC communication between an Android application and Tangem card. @@ -38,7 +38,7 @@ class NfcReader : CardReader { // 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(TaskError.UserCancelled())) + callback?.invoke(CompletionResult.Failure(SessionError.UserCancelled())) } } @@ -67,7 +67,7 @@ class NfcReader : CardReader { private fun transceiveData() { if (readingCancelled) { - callback?.invoke(CompletionResult.Failure(TaskError.UserCancelled())) + callback?.invoke(CompletionResult.Failure(SessionError.UserCancelled())) return } if (data == null) return @@ -76,7 +76,7 @@ class NfcReader : CardReader { try { rawResponse = isoDep?.transceive(data) } catch (exception: TagLostException) { - callback?.invoke(CompletionResult.Failure(TaskError.TagLost())) + callback?.invoke(CompletionResult.Failure(SessionError.TagLost())) isoDep = null return } catch (exception: Exception) { @@ -110,7 +110,7 @@ class NfcReader : CardReader { when (response) { is SlixReadResult.Failure -> { Log.e(this::class.simpleName!!, "${response.exception.message}") - callback?.invoke(CompletionResult.Failure(TaskError.ErrorProcessingCommand())) + callback?.invoke(CompletionResult.Failure(SessionError.ErrorProcessingCommand())) } is SlixReadResult.Success -> { callback?.invoke(CompletionResult.Success(ResponseApdu(response.data))) diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/SlixTagReader.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/SlixTagReader.kt index 587ded11a3..79f99b4eba 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/SlixTagReader.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/SlixTagReader.kt @@ -7,7 +7,7 @@ import com.tangem.common.apdu.StatusWord import com.tangem.common.extensions.toByteArray import com.tangem.common.extensions.toHexString import com.tangem.common.tlv.Tlv -import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvTag import java.io.ByteArrayOutputStream import java.io.IOException @@ -64,9 +64,9 @@ class SlixTagReader() { val areaBuf = readMultipleBlocks(1, blocksCount) - val tlvNdef = TlvMapper(Tlv.deserialize(areaBuf, true) ?: listOf()) + val tlvNdef = TlvDecoder(Tlv.deserialize(areaBuf, true) ?: listOf()) - return NdefMessage(tlvNdef.map(TlvTag.CardPublicKey)) + return NdefMessage(tlvNdef.decode(TlvTag.CardPublicKey)) } private fun readSingleBlock(blockNo: Int): ByteArray {