Updated on 2026-08-14

This commit is contained in:
Tangem 2019-11-25 13:53:30 +03:00
parent fe16bb4bcf
commit 8d716f97ed
11 changed files with 256 additions and 41 deletions

View file

@ -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
@ -81,6 +78,28 @@ class CardManager(
runTask(task, cardId, callback)
}
fun readIssuerData(cardId: String,
callback: (result: TaskEvent<ReadIssuerDataResponse>) -> Unit) {
val getIssuerDataCommand = ReadIssuerDataCommand(cardId)
val task = SingleCommandTask(getIssuerDataCommand)
runTask(task, cardId, callback)
}
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)
}
/**
*/

View file

@ -0,0 +1,78 @@
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,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers 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 issuers 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 issuers attestation data.
*/
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()
}
}
}

View file

@ -0,0 +1,61 @@
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 issuers 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 issuers attestation data.
*/
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()
}
}
}

View file

@ -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),

View file

@ -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)

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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
}
}
}
}

View file

@ -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>

View file

@ -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"