diff --git a/.gitignore b/.gitignore index 16e0697372..ae9ed9c524 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,22 @@ *.iml -.gradle -/local.properties -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -.DS_Store + +# Built application files /build -/captures -.externalNativeBuild -.idea/vcs.xml -.idea/caches -.idea/dictionaries -.idea/runConfigurations.xml -.idea/encodings.xml -.idea/codeStyles/codeStyleConfig.xml -.idea/assetWizardSettings.xml + +# Local configuration file (sdk path, etc) +local.properties + +# Gradle generated files +.gradle + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/*.xml + +# OS-specific files +.DS_Store +.DS_Store? # fastlane files **/fastlane/report.xml diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt index 62c88bf19f..4094d935bc 100644 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ b/tangem-core/src/main/java/com/tangem/CardManager.kt @@ -176,6 +176,46 @@ class CardManager( 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. @@ -210,8 +250,7 @@ class CardManager( /** */ - fun runTask(task: Task, cardId: String? = null, - callback: (result: TaskEvent) -> Unit) { + fun runTask(task: Task, cardId: String? = null, callback: (result: TaskEvent) -> Unit) { if (isBusy) { callback(TaskEvent.Completion(TaskError.Busy())) return diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt new file mode 100644 index 0000000000..3628d9cad7 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt @@ -0,0 +1,79 @@ +package com.tangem.commands + +import com.tangem.common.CardEnvironment +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.TlvTag +import com.tangem.tasks.TaskError + +/** +[REDACTED_AUTHOR] + */ +class ReadUserDataResponse( + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, + + /** + * Data defined by user's App. + */ + val userData: ByteArray, + + /** + * Data defined by user's App (confirmed by PIN2). + */ + val userProtectedData: ByteArray, + + /** + * Counter initialized by user's App and increased on every signing of new transaction + */ + val userCounter: Int, + + /** + * Counter initialized by user's App (confirmed by PIN2) and increased on every signing of new transaction + */ + val userProtectedCounter: Int + +): CommandResponse + +/** + * 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. + */ +class ReadUserDataCommand: CommandSerializer() { + + override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + val builder = TlvBuilder() + builder.append(TlvTag.CardId, cardEnvironment.cardId) + builder.append(TlvTag.Pin, cardEnvironment.pin1) + + return CommandApdu(Instruction.ReadUserData, builder.serialize()) + } + + override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadUserDataResponse? { + val tlvData = responseApdu.getTlvData() ?: return null + + 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) + ) + } catch (exception: Exception) { + throw TaskError.SerializeCommandError() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt new file mode 100644 index 0000000000..d512c4ded7 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt @@ -0,0 +1,62 @@ +package com.tangem.commands + +import com.tangem.common.CardEnvironment +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.TlvTag +import com.tangem.tasks.TaskError + +/** +[REDACTED_AUTHOR] + */ + +class WriteUserDataResponse( + /** + * 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. + * 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. + */ +class WriteUserDataCommand(private val userData: ByteArray? = null, private val userProtectedData: ByteArray? = null, + private val userCounter: Int? = null, + private val userProtectedCounter: Int? = null): CommandSerializer() { + + 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) + + return CommandApdu(Instruction.WriteUserData, builder.serialize()) + } + + override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteUserDataResponse? { + val tlvData = responseApdu.getTlvData() ?: return null + + return try { + WriteUserDataResponse(TlvMapper(tlvData).map(TlvTag.CardId)) + } catch (exception: Exception) { + throw TaskError.SerializeCommandError() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/apdu/Instruction.kt b/tangem-core/src/main/java/com/tangem/common/apdu/Instruction.kt index f0743aa0dc..a913813eb2 100644 --- a/tangem-core/src/main/java/com/tangem/common/apdu/Instruction.kt +++ b/tangem-core/src/main/java/com/tangem/common/apdu/Instruction.kt @@ -18,7 +18,9 @@ enum class Instruction(var code: Int) { Sign(0xFB), PurgeWallet(0xFC), Activate(0xFE), - OpenSession(0xFF); + OpenSession(0xFF), + WriteUserData(0xE0), + ReadUserData(0xE1); companion object { diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt index e5320b77b1..d37500e221 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt @@ -94,7 +94,6 @@ enum class TlvTag(val code: Int) { ProductMask(0x8A), PaymentFlowVersion(0x54), - UserCounter(0x2C), TokenSymbol(0xA0), @@ -107,7 +106,12 @@ enum class TlvTag(val code: Int) { TerminalIsLinked(0x58), TerminalPublicKey(0x5C), - TerminalTransactionSignature(0x57); + TerminalTransactionSignature(0x57), + + UserData(0x2A), + UserProtectedData(0x2B), + UserCounter(0x2C), + UserProtectedCounter(0x2D); /** * @return [TlvValueType] associated with a [TlvTag] @@ -121,7 +125,7 @@ enum class TlvTag(val code: Int) { MaxSignatures, PauseBeforePin2, RemainingSignatures, SignedHashes, Health, TokenDecimal, Offset, Size -> TlvValueType.Uint16 - UserCounter, IssuerDataCounter -> TlvValueType.Uint32 + UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32 IsActivated, TerminalIsLinked -> TlvValueType.BoolValue ManufactureDateTime -> TlvValueType.DateTime ProductMask -> TlvValueType.ProductMask diff --git a/tangem-demo/src/main/AndroidManifest.xml b/tangem-demo/src/main/AndroidManifest.xml index 2235a4e852..f90fa6c597 100644 --- a/tangem-demo/src/main/AndroidManifest.xml +++ b/tangem-demo/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> + @@ -46,6 +47,32 @@ android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" /> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt index 967c62c688..01207d703d 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt @@ -1,13 +1,9 @@ package com.tangem.tangemtest +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.tangem.CardManager -import com.tangem.common.extensions.hexToBytes -import com.tangem.common.extensions.toByteArray -import com.tangem.common.extensions.toHexString -import com.tangem.crypto.CryptoUtils -import com.tangem.crypto.sign import com.tangem.tangem_sdk_new.extensions.init import com.tangem.tasks.ScanEvent import com.tangem.tasks.TaskError @@ -154,6 +150,7 @@ class MainActivity : AppCompatActivity() { } } } + btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) } } private fun createSampleHashes(): Array { diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/TestUserDataActivity.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/TestUserDataActivity.kt new file mode 100644 index 0000000000..5b187df946 --- /dev/null +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/TestUserDataActivity.kt @@ -0,0 +1,180 @@ +package com.tangem.tangemtest + +import android.os.Bundle +import android.view.View +import android.widget.CompoundButton +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.tangem.CardManager +import com.tangem.commands.ReadUserDataResponse +import com.tangem.commands.WriteUserDataResponse +import com.tangem.common.CardEnvironment +import com.tangem.tangem_sdk_new.extensions.init +import com.tangem.tasks.ScanEvent +import com.tangem.tasks.TaskError +import com.tangem.tasks.TaskEvent +import kotlinx.android.synthetic.main.activity_test_user_data.* +import java.nio.charset.StandardCharsets + +/** +[REDACTED_AUTHOR] + */ +class TestUserDataActivity: AppCompatActivity() { + + private lateinit var cardManager: CardManager + private lateinit var writeOptions: WriteOptions + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_test_user_data) + + init() + initWriteOptions() + } + + private fun init() { + cardManager = CardManager.init(this) + + btn_scan?.setOnClickListener { _ -> + cardManager.scanCard { taskEvent -> + when (taskEvent) { + is TaskEvent.Event -> { + when (taskEvent.data) { + is ScanEvent.OnReadEvent -> { + // Handle returned card data + writeOptions.cardId = (taskEvent.data as ScanEvent.OnReadEvent).card.cardId + runOnUiThread { showReadWriteSection(true) } + } + is ScanEvent.OnVerifyEvent -> { + //Handle card verification + } + } + } + is TaskEvent.Completion -> { + if (taskEvent.error != null) { + if (taskEvent.error is TaskError.UserCancelled) { + // Handle case when user cancelled manually + } + // Handle other errors + } + // Handle completion + } + } + } + } + + btn_write.setOnClickListener { + if (writeOptions.cardId == null) return@setOnClickListener + + cardManager.writeUserData( + writeOptions.cardId !!, + writeOptions.userData, + writeOptions.userProtectedData, + writeOptions.userCounter, + writeOptions.userProtectedCounter + ) { + when (it) { + is TaskEvent.Completion -> handleError(tv_write_result, it.error) + is TaskEvent.Event -> { + runOnUiThread { + val data = it.data as? WriteUserDataResponse + if (data == null) { + tv_write_result.text = "Response doesn't match" + return@runOnUiThread + } + tv_write_result?.text = "Success" + } + } + } + } + } + + btn_read.setOnClickListener { + if (writeOptions.cardId == null) return@setOnClickListener + + cardManager.readUserData(writeOptions.cardId !!) { + when (it) { + is TaskEvent.Completion -> handleError(tv_read_result, it.error) + is TaskEvent.Event -> { + runOnUiThread { + val data = it.data as? ReadUserDataResponse + if (data == null) { + tv_read_result.text = "Response doesn't match" + return@runOnUiThread + } + tv_read_result?.text = "Success" + + writeOptions.userData = data.userData + writeOptions.userProtectedData = data.userProtectedData + writeOptions.userCounter = data.userCounter + writeOptions.userProtectedCounter = data.userProtectedCounter + + tv_card_cid.text = data.cardId + tv_data.text = String(data.userData, StandardCharsets.US_ASCII) + tv_protected_data.text = String(data.userProtectedData, StandardCharsets.US_ASCII) + tv_counter.text = data.userCounter.toString() + tv_protected_counter.text = data.userProtectedCounter.toString() + } + + } + } + } + } + } + + private fun handleError(tv: TextView, error: TaskError?) { + val er = error ?: return + if (er is TaskError.UserCancelled) return + + runOnUiThread { tv.text = er::class.simpleName } + } + + private fun initWriteOptions() { + writeOptions = WriteOptions() + + chb_with_ud.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateData(buttonView) } + chb_with_ud_protected.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedData(buttonView) } + chb_with_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateCounter(buttonView) } + chb_with_protected_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedCounter(buttonView) } + chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) } + } + + private fun showReadWriteSection(show: Boolean) { + val state = if (show) View.VISIBLE else View.GONE + cl_read_write.visibility = state + } +} + +class WriteOptions { + var cardId: String? = null + var userData: ByteArray? = null + var userProtectedData: ByteArray? = null + var userCounter: Int? = null + var userProtectedCounter: Int? = null + var pin2: String? = null + + fun updateData(chbx: CompoundButton) { + val value = "simple user data".toByteArray() + userData = if (chbx.isChecked) value else null + } + + fun updateProtectedData(chbx: CompoundButton) { + val value = "protected user data".toByteArray() + userProtectedData = if (chbx.isChecked) value else null + } + + fun updateCounter(chbx: CompoundButton) { + val value = if (userCounter == null) 0 else userCounter !! + 1 + userCounter = if (chbx.isChecked) value else null + } + + fun updateProtectedCounter(chbx: CompoundButton) { + val value = if (userProtectedCounter == null) 0 else userProtectedCounter !! + 1 + userProtectedCounter = if (chbx.isChecked) value else null + } + + fun updatePin2(chbx: CompoundButton) { + val value = CardEnvironment.DEFAULT_PIN2 + pin2 = if (chbx.isChecked) value else null + } +} \ No newline at end of file diff --git a/tangem-demo/src/main/res/layout/activity_main.xml b/tangem-demo/src/main/res/layout/activity_main.xml index f11cbdbd35..2af3ad92de 100644 --- a/tangem-demo/src/main/res/layout/activity_main.xml +++ b/tangem-demo/src/main/res/layout/activity_main.xml @@ -1,101 +1,116 @@ - + + + android:id="@+id/tv_card_cid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="48dp" + android:layout_marginBottom="80dp" + android:padding="16dp" + android:paddingBottom="48dp" + android:textAppearance="@style/TextAppearance.AppCompat.Large" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" />