Updated on 2026-08-14
This commit is contained in:
commit
3724f920de
11 changed files with 285 additions and 43 deletions
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem
|
||||
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.commands.SignCommand
|
||||
import com.tangem.commands.SignResponse
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.tasks.*
|
||||
import java.util.concurrent.Executors
|
||||
|
|
@ -40,6 +37,8 @@ class CardManager(
|
|||
* [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<ScanEvent>) -> Unit) {
|
||||
val task = ScanTask()
|
||||
|
|
@ -61,8 +60,8 @@ class CardManager(
|
|||
* 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
|
||||
*
|
||||
* @param callback is triggered on the completion of the [SignCommand],
|
||||
* provides card response in the form of [SignResponse].
|
||||
*/
|
||||
fun sign(hashes: Array<ByteArray>, cardId: String,
|
||||
callback: (result: TaskEvent<SignResponse>) -> Unit) {
|
||||
|
|
@ -81,6 +80,48 @@ class CardManager(
|
|||
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<ReadIssuerDataResponse>) -> Unit) {
|
||||
val getIssuerDataCommand = ReadIssuerDataCommand(cardId)
|
||||
val task = SingleCommandTask(getIssuerDataCommand)
|
||||
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 (which is kept on card).
|
||||
* @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<WriteIssuerDataResponse>) -> Unit) {
|
||||
val writeIssuerDataCommand = WriteIssuerDataCommand(
|
||||
cardId,
|
||||
issuerData,
|
||||
issuerDataSignature,
|
||||
issuerDataCounter)
|
||||
val task = SingleCommandTask(writeIssuerDataCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
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 ReadIssuerDataResponse(
|
||||
|
||||
/**
|
||||
* CID, Unique Tangem card ID number.
|
||||
*/
|
||||
val cardId: String,
|
||||
|
||||
/**
|
||||
* Data defined by issuer.
|
||||
*/
|
||||
val issuerData: ByteArray,
|
||||
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
||||
* SHA256([cardId] | [issuerData]).
|
||||
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
||||
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
||||
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
||||
*/
|
||||
val issuerDataSignature: ByteArray,
|
||||
|
||||
/**
|
||||
* An optional counter that protect issuer data against replay attack.
|
||||
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask]
|
||||
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
||||
*/
|
||||
val issuerDataCounter: Int?
|
||||
) : CommandResponse
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
*/
|
||||
class ReadIssuerDataCommand(
|
||||
private val cardId: String
|
||||
) : CommandSerializer<ReadIssuerDataResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = listOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes())
|
||||
)
|
||||
return CommandApdu(Instruction.ReadIssuerData, tlvData)
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerDataResponse? {
|
||||
val tlvData = responseApdu.getTlvData() ?: return null
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
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.extensions.toByteArray
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
class WriteIssuerDataResponse(
|
||||
/**
|
||||
* CID, Unique Tangem card ID number.
|
||||
*/
|
||||
val cardId: String
|
||||
) : CommandResponse
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
* @property issuerData Data provided by issuer.
|
||||
* @property issuerDataSignature Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* @property issuerDataCounter An optional counter that protect issuer data against replay attack.
|
||||
*/
|
||||
class WriteIssuerDataCommand(
|
||||
private val cardId: String,
|
||||
private val issuerData: ByteArray,
|
||||
private val issuerDataSignature: ByteArray,
|
||||
private val issuerDataCounter: Int? = null
|
||||
) : CommandSerializer<WriteIssuerDataResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = mutableListOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.IssuerData, issuerData),
|
||||
Tlv(TlvTag.IssuerDataSignature, issuerDataSignature)
|
||||
)
|
||||
if (issuerDataCounter != null) {
|
||||
tlvData.add(Tlv(TlvTag.IssuerDataCounter, issuerDataCounter.toByteArray()))
|
||||
}
|
||||
|
||||
return CommandApdu(Instruction.WriteIssuerData, tlvData)
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? {
|
||||
val tlvData = responseApdu.getTlvData() ?: return null
|
||||
|
||||
return try {
|
||||
val mapper = TlvMapper(tlvData)
|
||||
WriteIssuerDataResponse(
|
||||
cardId = mapper.map(TlvTag.CardId)
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
throw TaskError.SerializeCommandError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ enum class Instruction(var code: Int) {
|
|||
ValidateCard(0xF4),
|
||||
VerifyCode(0xF5),
|
||||
WriteIssuerData(0xF6),
|
||||
GetIssuerData(0xF7),
|
||||
ReadIssuerData(0xF7),
|
||||
CreateWallet(0xF8),
|
||||
CheckWallet(0xF9),
|
||||
SwapPIN(0xFA),
|
||||
|
|
|
|||
|
|
@ -33,3 +33,4 @@ fun ByteArray.toDate(): Date {
|
|||
|
||||
fun ByteArray.calculateSha512(): ByteArray = MessageDigest.getInstance("SHA-512").digest(this)
|
||||
|
||||
fun ByteArray.calculateSha256(): ByteArray = MessageDigest.getInstance("SHA-256").digest(this)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun Int.toByteArray(): ByteArray {
|
||||
val bufferSize = Int.SIZE_BYTES
|
||||
val buffer = ByteBuffer.allocate(bufferSize)
|
||||
buffer.putInt(this)
|
||||
return buffer.array()
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
||||
class IntExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `small int toByteArray`() {
|
||||
val int = 13
|
||||
val expected = byteArrayOf(0, 0, 0, 13)
|
||||
assertThat(int.toByteArray())
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `int toByteArray`() {
|
||||
val int = 999
|
||||
val expected = byteArrayOf(0, 0, 3, -25)
|
||||
assertThat(int.toByteArray())
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,8 @@ package com.tangem.common.tlv
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
class TlvTest {
|
||||
|
||||
|
|
@ -106,31 +100,4 @@ class TlvTest {
|
|||
assertThat(tlvs1)
|
||||
.isNull()
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun `sldfj dslkf`() {
|
||||
val privateKey = "C9A8827B83D554C2ED6C92A958A75691221A951775C36EBA858C039564ECDC3E"
|
||||
val password = "theXlPiP42".calculateSha256()
|
||||
|
||||
val originalKey: SecretKey = SecretKeySpec(password, 0, password.size, "AES")
|
||||
|
||||
CryptoUtils.initCrypto()
|
||||
|
||||
fun decrypt(yourKey: SecretKey): ByteArray {
|
||||
val decrypted: ByteArray
|
||||
val cipher = Cipher.getInstance("AES", "SC")
|
||||
cipher.init(Cipher.DECRYPT_MODE, yourKey, IvParameterSpec(ByteArray(cipher.blockSize)))
|
||||
decrypted = cipher.doFinal(privateKey.hexToBytes())
|
||||
return decrypted
|
||||
}
|
||||
|
||||
|
||||
val publicKey = CryptoUtils.generatePublicKey(
|
||||
decrypt(originalKey)
|
||||
)
|
||||
|
||||
assertThat(publicKey.toHexString())
|
||||
.isEqualTo("041D020A29983E2BC36E6CEAD9D648A71AA54AD328F00D6336DD699BCEBD99B5F4DAAAAC5E8E6D72D0A94EA86CF0A41D367FE05DE4A1BD84C86D51CCA4AF357182")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ class MainActivity : AppCompatActivity() {
|
|||
private val cardManager = CardManager(nfcManager.reader, cardManagerDelegate)
|
||||
|
||||
private lateinit var cardId: String
|
||||
private lateinit var issuerData: ByteArray
|
||||
private lateinit var issuerDataSignature: ByteArray
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -42,6 +44,7 @@ class MainActivity : AppCompatActivity() {
|
|||
runOnUiThread {
|
||||
tv_card_cid?.text = cardId
|
||||
btn_sign.isEnabled = true
|
||||
btn_read_issuer_data.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +69,37 @@ class MainActivity : AppCompatActivity() {
|
|||
is TaskEvent.Completion -> {
|
||||
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
|
||||
}
|
||||
is TaskEvent.Event -> runOnUiThread { tv_card_cid?.text = cardId + " used to sign sample hashes." }
|
||||
is TaskEvent.Event -> runOnUiThread { tv_card_cid?.text = cardId + "was used to sign sample hashes." }
|
||||
}
|
||||
}
|
||||
}
|
||||
btn_read_issuer_data?.setOnClickListener { _ ->
|
||||
cardManager.readIssuerData(cardId) {
|
||||
when (it) {
|
||||
is TaskEvent.Completion -> {
|
||||
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
|
||||
}
|
||||
is TaskEvent.Event -> runOnUiThread {
|
||||
btn_write_issuer_data.isEnabled = true
|
||||
tv_card_cid?.text = it.data.issuerData.contentToString()
|
||||
issuerData = it.data.issuerData
|
||||
issuerDataSignature = it.data.issuerDataSignature
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
btn_write_issuer_data?.setOnClickListener { _ ->
|
||||
cardManager.writeIssuerData(
|
||||
cardId,
|
||||
issuerData,
|
||||
issuerDataSignature) {
|
||||
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.cardId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
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"
|
||||
|
|
@ -40,6 +41,27 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/btn_scan"
|
||||
android:enabled="false"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_read_issuer_data"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Read issuer data"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btn_sign"
|
||||
android:enabled="false"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_write_issuer_data"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Write issuer data"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data"
|
||||
android:enabled="false"/>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
<TextView
|
||||
android:id="@+id/tvTaskText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="100dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue