Updated on 2026-08-14
This commit is contained in:
parent
b7f323fd14
commit
7c70545ae7
10 changed files with 117 additions and 77 deletions
|
|
@ -10,6 +10,7 @@ import com.tangem.common.tlv.Tlv
|
|||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
/**
|
||||
|
|
@ -23,7 +24,16 @@ class CheckWalletResponse(
|
|||
val cardId: String,
|
||||
val salt: ByteArray,
|
||||
val walletSignature: ByteArray
|
||||
) : CommandResponse
|
||||
) : CommandResponse {
|
||||
|
||||
fun verify(curve: EllipticCurve, publicKey: ByteArray, challenge: ByteArray): Boolean {
|
||||
return CryptoUtils.verify(
|
||||
publicKey,
|
||||
challenge + salt,
|
||||
walletSignature,
|
||||
curve)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This command proves that the wallet private key from the card corresponds to the wallet public key.
|
||||
|
|
@ -33,15 +43,14 @@ class CheckWalletResponse(
|
|||
* @property cardId Unique Tangem card ID number
|
||||
* @property challenge Random challenge generated by application
|
||||
*/
|
||||
class CheckWalletCommand(
|
||||
private val cardId: String,
|
||||
private val challenge: ByteArray
|
||||
) : CommandSerializer<CheckWalletResponse>() {
|
||||
class CheckWalletCommand : CommandSerializer<CheckWalletResponse>() {
|
||||
|
||||
val challenge = CryptoUtils.generateRandomBytes(16)
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.Challenge, challenge)
|
||||
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,14 +38,12 @@ class CreateWalletResponse(
|
|||
*
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
*/
|
||||
class CreateWalletCommand(
|
||||
private val cardId: String
|
||||
) : CommandSerializer<CreateWalletResponse>() {
|
||||
class CreateWalletCommand : CommandSerializer<CreateWalletResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
|
||||
tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc)
|
||||
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
|
||||
|
|
|
|||
|
|
@ -30,14 +30,12 @@ class PurgeWalletResponse(
|
|||
* ‘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>() {
|
||||
class PurgeWalletCommand : CommandSerializer<PurgeWalletResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
|
||||
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,14 +50,12 @@ class ReadIssuerDataResponse(
|
|||
* 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>() {
|
||||
class ReadIssuerDataCommand : CommandSerializer<ReadIssuerDataResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class SignResponse(
|
|||
* @property hashes Array of transaction hashes.
|
||||
* @property cardId CID, Unique Tangem card ID number
|
||||
*/
|
||||
class SignCommand(private val hashes: Array<ByteArray>, private val cardId: String)
|
||||
class SignCommand(private val hashes: Array<ByteArray>)
|
||||
: CommandSerializer<SignResponse>() {
|
||||
|
||||
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
|
||||
|
|
@ -54,7 +54,7 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
|
|||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
|
||||
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte()))
|
||||
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)
|
||||
tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ class WriteIssuerDataResponse(
|
|||
* @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
|
||||
|
|
@ -40,7 +39,7 @@ class WriteIssuerDataCommand(
|
|||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.IssuerData, issuerData)
|
||||
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
|
||||
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CardStatus
|
||||
import com.tangem.commands.CheckWalletCommand
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -31,64 +32,47 @@ sealed class ScanEvent {
|
|||
internal class ScanTask : Task<ScanEvent>() {
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
currentCard: Card?,
|
||||
callback: (result: TaskEvent<ScanEvent>) -> Unit) {
|
||||
|
||||
val readCommand = ReadCommand()
|
||||
sendCommand(readCommand, cardEnvironment) { readResult ->
|
||||
if (currentCard != null) callback(TaskEvent.Event(ScanEvent.OnReadEvent(currentCard)))
|
||||
|
||||
when (readResult) {
|
||||
if (currentCard == null) {
|
||||
completeNfcSession(true, TaskError.MissingPreflightRead())
|
||||
callback(TaskEvent.Completion(TaskError.MissingPreflightRead()))
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
if (readResult.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, readResult.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(readResult.error))
|
||||
}
|
||||
} else if (currentCard.status != CardStatus.Loaded) {
|
||||
completeNfcSession()
|
||||
callback(TaskEvent.Completion())
|
||||
|
||||
is CompletionResult.Success -> {
|
||||
val card = readResult.data
|
||||
} else if (currentCard.curve == null || currentCard.walletPublicKey == null) {
|
||||
completeNfcSession(true, TaskError.CardError())
|
||||
callback(TaskEvent.Completion(TaskError.CardError()))
|
||||
|
||||
callback(TaskEvent.Event(ScanEvent.OnReadEvent(card)))
|
||||
} else {
|
||||
|
||||
if (card.curve == null || card.walletPublicKey == null) {
|
||||
completeNfcSession(true)
|
||||
callback(TaskEvent.Completion(TaskError.CardError()))
|
||||
return@sendCommand
|
||||
}
|
||||
val checkWalletCommand = CheckWalletCommand()
|
||||
|
||||
val challenge = CryptoUtils.generateRandomBytes(16)
|
||||
val checkWalletCommand = CheckWalletCommand(
|
||||
card.cardId,
|
||||
challenge)
|
||||
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, result.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(result.error))
|
||||
}
|
||||
|
||||
is CompletionResult.Success -> {
|
||||
completeNfcSession()
|
||||
val checkWalletResponse = result.data
|
||||
val verified = CryptoUtils.verify(
|
||||
card.walletPublicKey,
|
||||
challenge + checkWalletResponse.salt,
|
||||
checkWalletResponse.walletSignature,
|
||||
card.curve)
|
||||
if (verified) {
|
||||
callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(true)))
|
||||
callback(TaskEvent.Completion())
|
||||
} else {
|
||||
callback(TaskEvent.Completion(TaskError.VefificationFailed()))
|
||||
}
|
||||
}
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, 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())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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
|
||||
|
|
@ -14,8 +15,11 @@ class SingleCommandTask<Event : CommandResponse>(
|
|||
private val command: CommandSerializer<Event>
|
||||
) : Task<Event>() {
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<Event>) -> Unit) {
|
||||
override fun onRun(
|
||||
cardEnvironment: CardEnvironment,
|
||||
currentCard: Card?,
|
||||
callback: (result: TaskEvent<Event>) -> Unit
|
||||
) {
|
||||
sendCommand(command, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.CardManagerDelegate
|
||||
import com.tangem.CardReader
|
||||
import com.tangem.Log
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.StatusWord
|
||||
|
|
@ -32,12 +34,15 @@ sealed class TaskError(description: String? = null) : Exception(description) {
|
|||
class VefificationFailed : TaskError()
|
||||
class CardError : TaskError()
|
||||
class ReaderError() : TaskError()
|
||||
class SerializeCommandError() : TaskError()
|
||||
class SerializeCommandError(description: String? = null) : TaskError(description)
|
||||
|
||||
class CardIsMissing() : TaskError()
|
||||
class EmptyHashes() : TaskError()
|
||||
class TooMuchHashes() : TaskError()
|
||||
class HashSizeMustBeEqual() : TaskError()
|
||||
|
||||
class WrongCard() : TaskError()
|
||||
class MissingPreflightRead() : TaskError()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,6 +72,7 @@ abstract class Task<T> {
|
|||
|
||||
var delegate: CardManagerDelegate? = null
|
||||
var reader: CardReader? = null
|
||||
var performPreflightRead: Boolean = true
|
||||
|
||||
/**
|
||||
* This method should be called to run the [Task] and perform all its operations.
|
||||
|
|
@ -79,7 +85,12 @@ abstract class Task<T> {
|
|||
delegate?.onNfcSessionStarted()
|
||||
reader?.openSession()
|
||||
Log.i(this::class.simpleName!!, "Nfc task is started")
|
||||
onRun(cardEnvironment, callback)
|
||||
|
||||
if (performPreflightRead) {
|
||||
runWithPreflightRead(cardEnvironment, callback)
|
||||
} else {
|
||||
onRun(cardEnvironment, null, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,6 +112,7 @@ abstract class Task<T> {
|
|||
* In this method the individual Tasks' logic should be implemented.
|
||||
*/
|
||||
protected abstract fun onRun(cardEnvironment: CardEnvironment,
|
||||
currentCard: Card?,
|
||||
callback: (result: TaskEvent<T>) -> Unit)
|
||||
|
||||
/**
|
||||
|
|
@ -165,6 +177,34 @@ abstract class Task<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runWithPreflightRead(
|
||||
environment: CardEnvironment, callback: (result: TaskEvent<T>) -> Unit) {
|
||||
sendCommand(ReadCommand(), environment) { readResult ->
|
||||
when (readResult) {
|
||||
is CompletionResult.Failure -> {
|
||||
completeNfcSession(true, readResult.error)
|
||||
callback(TaskEvent.Completion(readResult.error))
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
|
||||
val receivedCardId = readResult.data.cardId
|
||||
|
||||
if (environment.cardId != null && environment.cardId != receivedCardId) {
|
||||
completeNfcSession(true, TaskError.WrongCard())
|
||||
callback(TaskEvent.Completion(TaskError.WrongCard()))
|
||||
return@sendCommand
|
||||
}
|
||||
|
||||
val newEnvironment = environment.copy(cardId = receivedCardId)
|
||||
onRun(newEnvironment, readResult.data, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import com.tangem.CardManager
|
||||
import com.tangem.tangem_sdk_new.DefaultCardManagerDelegate
|
||||
import com.tangem.tangem_sdk_new.NfcLifecycleObserver
|
||||
import com.tangem.tangem_sdk_new.TerminalKeysStorage
|
||||
import com.tangem.tangem_sdk_new.nfc.NfcManager
|
||||
import com.tangem.tasks.ScanEvent
|
||||
import com.tangem.tasks.TaskError
|
||||
|
|
@ -15,7 +16,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private val nfcManager = NfcManager()
|
||||
private val cardManagerDelegate: DefaultCardManagerDelegate = DefaultCardManagerDelegate(nfcManager.reader)
|
||||
private val cardManager = CardManager(nfcManager.reader, cardManagerDelegate)
|
||||
private val cardManager = CardManager(nfcManager.reader, cardManagerDelegate, TerminalKeysStorage())
|
||||
|
||||
private lateinit var cardId: String
|
||||
private lateinit var issuerData: ByteArray
|
||||
|
|
@ -38,6 +39,10 @@ class MainActivity : AppCompatActivity() {
|
|||
is ScanEvent.OnReadEvent -> {
|
||||
// Handle returned card data
|
||||
cardId = (taskEvent.data as ScanEvent.OnReadEvent).card.cardId
|
||||
runOnUiThread {
|
||||
tv_card_cid?.text = cardId
|
||||
btn_create_wallet.isEnabled = true
|
||||
}
|
||||
}
|
||||
is ScanEvent.OnVerifyEvent -> {
|
||||
//Handle card verification
|
||||
|
|
@ -127,6 +132,11 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
is TaskEvent.Event -> runOnUiThread {
|
||||
tv_card_cid?.text = it.data.status.name
|
||||
btn_sign.isEnabled = true
|
||||
btn_read_issuer_data.isEnabled = true
|
||||
btn_purge_wallet.isEnabled = true
|
||||
btn_create_wallet.isEnabled = false
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue