Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-12 12:51:33 +04:00
parent 254b5cd08d
commit c540257c66
3 changed files with 149 additions and 2 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchain.extensions.encodeBase58
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -13,6 +14,7 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.error.parseTangemSdkError
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
@ -22,6 +24,10 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
private val TransactionSignAttemptException = IllegalStateException(
"solana_signMessage payload is a serialized Solana transaction; signing rejected ([REDACTED_TASK_KEY])",
)
internal class WcSolanaMessageSignUseCase @AssistedInject constructor(
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignMessage,
@ -38,6 +44,15 @@ internal class WcSolanaMessageSignUseCase @AssistedInject constructor(
state: WcSignState<WcMessageSignUseCase.SignModel>,
) {
val hashToSign = method.rawMessage.decodeBase58() ?: byteArrayOf()
// Never blind-sign a payload that is actually a serialized Solana transaction message — the
// resulting signature would be a valid transaction signature that a malicious dApp could broadcast to move
// the user's funds. Reject such requests instead of signing them.
if (SolanaTransactionHelper.isTransactionMessage(hashToSign)) {
emit(state.toResult(WcRequestError.UnknownError(TransactionSignAttemptException).left()))
return
}
val userWallet = session.wallet
val signedHash = signUseCase(hashToSign, userWallet, network)

View file

@ -0,0 +1,132 @@
package com.tangem.data.walletconnect.network.solana
import app.cash.turbine.test
import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.blockchain.extensions.encodeBase58
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.models.account.Account
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class WcSolanaMessageSignUseCaseTest {
private val signUseCase: SignUseCase = mockk()
private val respondService: WcRespondService = mockk()
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
private val context: WcMethodUseCaseContext = WcMethodUseCaseContext(
network = MockCryptoCurrencyFactory().ethereum.network,
accountAddress = "",
rawSdkRequest = WcSdkSessionRequest(
topic = "",
chainId = "",
dAppMetaData = WcAppMetaData(name = "", description = "", url = "", icons = listOf(), redirect = ""),
request = WcSdkSessionRequest.JSONRPCRequest(id = 0L, method = "", params = ""),
),
networkDerivationsCount = 1,
session = WcSession(
wallet = MockUserWalletFactory.create(),
networks = setOf(),
account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId),
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
connectingTime = 0L,
sdkModel = WcSdkSession(
topic = "",
namespaces = mapOf(),
appMetaData = WcAppMetaData(name = "", description = "", url = "", icons = listOf(), redirect = ""),
),
showWalletInfo = false,
),
)
@BeforeEach
fun setup() {
clearMocks(signUseCase, respondService)
}
@Test
fun `GIVEN payload is a serialized transaction WHEN sign THEN request rejected without signing`() =
runTest(UnconfinedTestDispatcher()) {
// Arrange
val useCase = createUseCase(rawMessage = LEGACY_TRANSACTION_MESSAGE.encodeBase58())
// Act
useCase.invoke().test {
awaitItem() // initial PreSign state
useCase.sign()
val result = (expectMostRecentItem().domainStep as WcSignStep.Result).result
// Assert
assertTrue(result.isLeft())
assertTrue(result.leftOrNull() is WcRequestError.UnknownError)
}
coVerify(exactly = 0) { signUseCase(any(), any(), any()) }
coVerify(exactly = 0) { respondService.respond(any(), any()) }
}
@Test
fun `GIVEN human-readable message WHEN sign THEN it is signed and responded`() =
runTest(UnconfinedTestDispatcher()) {
// Arrange
val message = "Sign in to Tangem\nNonce: 8f3a91c0d4".toByteArray()
coEvery { signUseCase(any(), any(), any()) } returns byteArrayOf(0x0A, 0x0B, 0x0C).right()
coEvery { respondService.respond(any(), any()) } returns RESPOND_RESULT.right()
val useCase = createUseCase(rawMessage = message.encodeBase58())
// Act
useCase.invoke().test {
awaitItem() // initial PreSign state
useCase.sign()
val result = (expectMostRecentItem().domainStep as WcSignStep.Result).result
// Assert
assertEquals(RESPOND_RESULT.right(), result)
}
coVerify(exactly = 1) { signUseCase(any(), context.session.wallet, context.network) }
coVerify(exactly = 1) { respondService.respond(any(), any()) }
}
private fun createUseCase(rawMessage: String) = WcSolanaMessageSignUseCase(
context = context,
method = WcSolanaMethod.SignMessage(pubKey = "", rawMessage = rawMessage, humanMsg = ""),
signUseCase = signUseCase,
respondService = respondService,
analytics = analytics,
)
private companion object {
const val RESPOND_RESULT = "{ signature: \"signature\" }"
// A minimal but well-formed legacy Solana message: header + 2 accounts + blockhash + 1 instruction.
val LEGACY_TRANSACTION_MESSAGE: ByteArray = byteArrayOf(0x01, 0x00, 0x01) + // message header
byteArrayOf(0x02) + ByteArray(size = 2 * 32) + // 2 account keys
ByteArray(size = 32) + // recent blockhash
byteArrayOf(0x01) + // instruction count
byteArrayOf(0x01) + // program id index
byteArrayOf(0x01, 0x00) + // 1 account index = [0]
byteArrayOf(0x03, 0x0A, 0x0B, 0x0C) // data length 3 + 3 data bytes
}
}

View file

@ -5,9 +5,9 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1559"
tangemBlockchainSdk = "develop-1566"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-620"
tangemCardSdk = "develop-624"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "tangem-master-21"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^