diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt index b8adc0c608..24a815b459 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt @@ -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, ) { 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) diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCaseTest.kt new file mode 100644 index 0000000000..1141ca8c9d --- /dev/null +++ b/data/wallet-connect/src/test/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCaseTest.kt @@ -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 + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1559b2ccdb..eef36fbda4 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -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 ^