Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-22 14:32:29 +00:00
commit a270251185
7 changed files with 215 additions and 4 deletions

View file

@ -10,7 +10,8 @@ data class CardEnvironment(
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionKey: ByteArray? = null
val encryptionKey: ByteArray? = null,
val cvc: ByteArray? = null
) {
companion object {

View file

@ -122,6 +122,37 @@ class CardManager(
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<CreateWalletResponse>) -> Unit) {
val createWalletCommand = CreateWalletCommand(cardId)
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<PurgeWalletResponse>) -> Unit) {
val purgeWalletCommand = PurgeWalletCommand(cardId)
val task = SingleCommandTask(purgeWalletCommand)
runTask(task, cardId, callback)
}
/**
*/
@ -157,6 +188,6 @@ class CardManager(
}
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
return cardEnvironmentRepository[cardId] ?: CardEnvironment()
return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId)
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
* 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.
*
* @property cardId CID, Unique Tangem card ID number.
*/
class CreateWalletCommand(
private val cardId: String
) : CommandSerializer<CreateWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
if (cardEnvironment.cvc != null) {
tlvData.add(Tlv(TlvTag.Cvc, cardEnvironment.cvc))
}
return CommandApdu(Instruction.CreateWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
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()
}
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class PurgeWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus
) : CommandResponse
/**
* 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.
* @property cardId CID, Unique Tangem card ID number.
*/
class PurgeWalletCommand(
private val cardId: String
) : CommandSerializer<PurgeWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
return CommandApdu(Instruction.PurgeWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
PurgeWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status))
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) {
Pause(0x1C),
ManufactureId(0x20),
ManufacturerSignature(0x21),
ManufacturerSignature(0x86),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),

View file

@ -45,6 +45,8 @@ class MainActivity : AppCompatActivity() {
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
}
}
@ -103,6 +105,32 @@ class MainActivity : AppCompatActivity() {
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
cardManager.purgeWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
btn_create_wallet?.setOnClickListener { _ ->
cardManager.createWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
}
private fun createSampleHashes(): Array<ByteArray> {

View file

@ -9,12 +9,12 @@
<TextView
android:paddingBottom="48dp"
android:layout_marginTop="48dp"
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_marginBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -25,6 +25,7 @@
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read and verify card"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -63,5 +64,27 @@
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_create_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Create wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_purge_wallet"
android:enabled="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>