Updated on 2026-08-14

This commit is contained in:
Tangem 2020-12-18 09:35:20 +00:00
commit c36eab59be
8 changed files with 107 additions and 57 deletions

View file

@ -13,6 +13,6 @@
},
{
"name": "isCreatingTwinCardsAllowed",
"value": false
"value": true
}
]

View file

@ -13,6 +13,6 @@
},
{
"name": "isCreatingTwinCardsAllowed",
"value": false
"value": true
}
]

View file

@ -7,20 +7,16 @@ import com.tangem.TangemSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.CommandResponse
import com.tangem.commands.ReadIssuerDataCommand
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.card.masks.Product
import com.tangem.commands.file.ReadFileDataCommand
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
@ -75,7 +71,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data
walletManager, card, verifyResult.data, publicKey
)))
}
is CompletionResult.Failure -> {
@ -89,20 +85,21 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
ReadFileDataCommand().run(session) { filesResult ->
when (filesResult) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val decoder = Tlv.deserialize(filesResult.data.fileData)?.let { TlvDecoder(it) }
val name = decoder?.decodeOptional<String>(TlvTag.FileName)
if (name != null && name == TwinsHelper.TWIN_FILE_NAME) {
val publicKey = decoder.decodeOptional<ByteArray>(TlvTag.FileData)?.toHexString()
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, card.walletPublicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
WalletManagerFactory.makeMultisigWalletManager(card, publicKey!!.hexToBytes())
WalletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
}
verifyCard(walletManager, card, publicKey, session, callback)
verifyCard(walletManager, card, twinPublicKey.toHexString(), session, callback)
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
@ -111,7 +108,6 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.common.CompletionResult
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.file.DeleteFilesTask
class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = false
@ -18,24 +17,13 @@ class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.copy(status = CardStatus.Empty)
finishTask(session, callback)
CreateWalletTask().run(session) { callback(it) }
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
finishTask(session, callback)
}
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
DeleteFilesTask().run(session) { deleteResponse ->
when (deleteResponse) {
is CompletionResult.Success ->
CreateWalletTask().run(session) { callback(it) }
is CompletionResult.Failure ->
callback(CompletionResult.Failure(deleteResponse.error))
}
CreateWalletTask().run(session) { callback(it) }
}
}
}

View file

@ -5,10 +5,9 @@ import com.tangem.CardSessionRunnable
import com.tangem.commands.CreateWalletResponse
import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.file.WriteFileDataCommand
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.file.DeleteFilesTask
class CreateSecondTwinWalletTask(private val firstPublicKey: String) : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = true
@ -31,20 +30,23 @@ class CreateSecondTwinWalletTask(private val firstPublicKey: String) : CardSessi
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
DeleteFilesTask().run(session) { deleteResponse ->
when (deleteResponse) {
CreateWalletTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val file = TwinCardsManager.createFileWithPublicKey(firstPublicKey)
WriteFileDataCommand(file).run(session) { result ->
when (result) {
is CompletionResult.Success -> CreateWalletTask().run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
session.environment.card =
session.environment.card?.copy(status = CardStatus.Loaded)
WriteProtectedIssuerDataTask(
firstPublicKey.hexToBytes(), TwinCardsManager.issuerKeys
).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(writeResult.error))
}
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Failure(deleteResponse.error))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -1,17 +1,14 @@
package com.tangem.tap.domain.twins
import com.tangem.KeyPair
import com.tangem.Message
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.file.FileData
import com.tangem.commands.file.WriteFileDataCommand
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.tlv.TlvEncoder
import com.tangem.common.tlv.TlvTag
import com.tangem.common.tlv.serialize
import com.tangem.crypto.CryptoUtils
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.tangemSdkManager
@ -54,7 +51,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
suspend fun complete(message: Message): Result<ScanNoteResponse> {
val response = tangemSdkManager.runTaskAsync(
WriteFileDataCommand(createFileWithPublicKey(secondCardPublicKey!!)),
WriteProtectedIssuerDataTask(secondCardPublicKey!!.hexToBytes(), issuerKeys),
currentCardId, message
)
return when (response) {
@ -72,11 +69,19 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
}
companion object {
fun createFileWithPublicKey(publicKey: String): FileData {
val nameTlv = TlvEncoder().encode(TlvTag.FileName, "TwinPublicKey")
val pubKey = TlvEncoder().encode(TlvTag.FileData, publicKey.hexToBytes())
val tlvs = listOf(nameTlv, pubKey).serialize()
return FileData.DataProtectedByPasscode(tlvs)
val issuerKeys = KeyPair(
privateKey = "F9F4C50636C9E6FC65F92655BD5C21C85A5F6A34DCD0F1E75FCEA1980FE242F5".hexToBytes(),
publicKey = ("048196AA4B410AC44A3B9CCE18E7BE226AEA070ACC83A9CF67540F" +
"AC49AF25129F6A538A28AD6341358E3C4F9963064F" +
"7E365372A651D374E5C23CDD37FD099BF2").hexToBytes()
)
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return (cardWalletPublicKey != null &&
CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey))
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.guard
import com.tangem.common.files.FileHashHelper
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
) : CardSessionRunnable<WriteIssuerDataResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val hashes = arrayOf(twinPublicKey.calculateSha256())
SignCommand(hashes).run(session) { signResult ->
when (signResult) {
is CompletionResult.Success -> {
ReadIssuerDataCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
writeIssuerData(
twinPublicKey, issuerKeys, signResult.data.signature,
readResult.data, session, callback
)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(signResult.error))
}
}
}
private fun writeIssuerData(
twinPublicKey: ByteArray, issuerKeys: KeyPair, cardSignature: ByteArray,
readResponse: ReadIssuerDataResponse,
session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>
) -> Unit) {
val cardId = session.environment.card?.cardId.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val counter = (readResponse.issuerDataCounter ?: 0) + 1
val data = twinPublicKey + cardSignature
val signedByIssuer = FileHashHelper.prepareHashes(
cardId, data, counter, issuerKeys.privateKey
)
WriteIssuerDataCommand(
data, signedByIssuer.finalizingSignature!!,
counter, issuerKeys.publicKey
).run(session, callback)
}
}

View file

@ -38,12 +38,12 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:gravity="start"
android:text="@string/details_twins_recreate_step_format"
android:textColor="@color/colorSecondary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/v_step_1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@id/v_step_1"
app:layout_constraintTop_toTopOf="parent" />