Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:WalletConnect/WalletConnectKotli…
Browse files Browse the repository at this point in the history
…nV2 into feat/automate_close_and_release_staging_repositories
  • Loading branch information
jakubuid committed Jul 23, 2024
2 parents 2bad6fb + 075bd71 commit a184ef9
Show file tree
Hide file tree
Showing 174 changed files with 2,883 additions and 1,321 deletions.
4 changes: 4 additions & 0 deletions core/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ dependencies {
testImplementation(libs.bundles.sqlDelight.test)
testImplementation(libs.koin.test)

androidTestImplementation(libs.mockk.android)
androidTestImplementation(libs.coroutines.test)
androidTestImplementation(libs.core)

androidTestUtil(libs.androidx.testOrchestrator)
androidTestImplementation(libs.bundles.androidxAndroidTest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.walletconnect.android

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.walletconnect.android.internal.common.JsonRpcResponse
import com.walletconnect.android.internal.common.crypto.codec.Codec
import com.walletconnect.android.internal.common.json_rpc.data.JsonRpcSerializer
import com.walletconnect.android.internal.common.json_rpc.domain.link_mode.LinkModeJsonRpcInteractor
import com.walletconnect.android.internal.common.json_rpc.model.JsonRpcHistoryRecord
import com.walletconnect.android.internal.common.model.EnvelopeType
import com.walletconnect.android.internal.common.model.Participants
import com.walletconnect.android.internal.common.model.sync.ClientJsonRpc
import com.walletconnect.android.internal.common.model.type.JsonRpcClientSync
import com.walletconnect.android.internal.common.storage.rpc.JsonRpcHistory
import com.walletconnect.android.internal.common.wcKoinApp
import com.walletconnect.foundation.common.model.Topic
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test

class LinkModeInteractorTests {
private val chaChaPolyCodec: Codec = mockk()
private val jsonRpcHistory: JsonRpcHistory = mockk()
private val context: Context = ApplicationProvider.getApplicationContext()
private val serializer: JsonRpcSerializer = mockk()
private val interactor: LinkModeJsonRpcInteractor

init {
mockkObject(wcKoinApp)

every { wcKoinApp.koin.get<JsonRpcSerializer>() } returns serializer
interactor = LinkModeJsonRpcInteractor(chaChaPolyCodec, jsonRpcHistory, context)
}

private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)

private val payload: JsonRpcClientSync<*> = mockk {
every { id } returns 1
every { method } returns "wc_sessionAuthenticate"
}

private val clientJsonRpc: ClientJsonRpc = mockk {
every { id } returns 1
every { method } returns "wc_sessionAuthenticate"
}

private val response: JsonRpcResponse = mockk {
every { id } returns 1
}

private val topic = Topic("test_topic")
private val appLink = "https://web3modal-laboratory-git-chore-kotlin-assetlinks-walletconnect1.vercel.app/wallet"
private val envelopeType = EnvelopeType.TWO
private val requestJson = """
{"id":1720520264638574,"jsonrpc":"2.0","method":"wc_sessionAuthenticate","params":{"requester":{"publicKey":"242f16c16b035d6f592a1438a37529cc2396bd9d0dee25eb9e94ac8104282a04","metadata":{"description":"Kotlin Dapp Implementation","url":"https://web3modal-laboratory-git-chore-kotlin-assetlinks-walletconnect1.vercel.app","icons":["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"],"name":"Kotlin Dapp","redirect":{"native":"kotlin-dapp-wc://request","universal":"https://web3modal-laboratory-git-chore-kotlin-assetlinks-walletconnect1.vercel.app/dapp","linkMode":true}}},"authPayload":{"type":"eip4361","chains":["eip155:1"],"domain":"sample.kotlin.dapp","aud":"https://web3inbox.com/all-apps","nonce":"44c2ec99fab82d65d7cf7e84","version":"1","iat":"2024-07-09T12:17:44+02:00","statement":"Sign in with wallet.","resources":["urn:recap:eyJhdHQiOnsiaHR0cHM6Ly9ub3RpZnkud2FsbGV0Y29ubmVjdC5jb20vYWxsLWFwcHMiOnsiY3J1ZC9zdWJzY3JpcHRpb25zIjpbe31dLCJjcnVkL25vdGlmaWNhdGlvbnMiOlt7fV19fX0=","ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/","urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3RcL3BlcnNvbmFsX3NpZ24iOlt7fV0sInJlcXVlc3RcL2V0aF9zaWduVHlwZWREYXRhIjpbe31dfSwiaHR0cHM6XC9cL25vdGlmeS53YWxsZXRjb25uZWN0LmNvbVwvYWxsLWFwcHMiOnsiY3J1ZFwvc3Vic2NyaXB0aW9ucyI6W3t9XSwiY3J1ZFwvbm90aWZpY2F0aW9ucyI6W3t9XX19fQ"]},"expiryTimestamp":1720523864}}
""".trimIndent()
private val encryptedResponse = "encrypted_response".toByteArray()

@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}

@OptIn(ExperimentalCoroutinesApi::class)
@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun testTriggerRequestWithValidData() = testScope.runTest {
every { serializer.serialize(payload) } returns requestJson
every { jsonRpcHistory.setRequest(any(), any(), any(), any(), any()) } returns true
every { chaChaPolyCodec.encrypt(any(), any(), any()) } returns encryptedResponse

interactor.triggerRequest(payload, topic, appLink, envelopeType)

verify {
serializer.serialize(payload)
chaChaPolyCodec.encrypt(topic, requestJson, envelopeType)
}
}

@Test
fun testTriggerRequestWithSerializationFailure() = testScope.runTest {
every { serializer.serialize(payload) } returns null

try {
interactor.triggerRequest(payload, topic, appLink, envelopeType)
} catch (e: IllegalStateException) {
assert(e.message == "LinkMode: Cannot serialize the request")
}
}

@Test
fun testTriggerResponseWithValidData() = testScope.runTest {
val participants: Participants? = null
val envelopeType = EnvelopeType.ZERO
val responseJson = "response_json"
val encryptedResponse = "encrypted_response".toByteArray()
val jsonRpcRecord: JsonRpcHistoryRecord = mockk()

every { serializer.serialize(response) } returns responseJson
every { chaChaPolyCodec.encrypt(any(), any(), any(), any()) } returns encryptedResponse
every { jsonRpcHistory.updateRequestWithResponse(any(), any()) } returns jsonRpcRecord

interactor.triggerResponse(topic, response, appLink, participants, envelopeType)

verify {
serializer.serialize(response)
chaChaPolyCodec.encrypt(topic, responseJson, envelopeType, participants)
}
}

@Test
fun testTriggerResponseWithSerializationFailure() = testScope.runTest {
val response: JsonRpcResponse = mockk()
val participants: Participants? = null
val envelopeType = EnvelopeType.ZERO

every { serializer.serialize(response) } returns null

try {
interactor.triggerResponse(topic, response, appLink, participants, envelopeType)
} catch (e: IllegalStateException) {
assert(e.message == "LinkMode: Cannot serialize the response")
}
}

@Ignore("Test failing in pipeline")
@Test
fun testDispatchEnvelopeWithValidData() = testScope.runTest {
val url =
"https://web3modal-laboratory-git-chore-kotlin-assetlinks-walletconnect1.vercel.app/dapp?wc_ev=AWCUZ9qZjOuuPXoeCspXBMFd8NXumb1ZoXemH4BPoA1L1_bsdepR39jMhAc2u9L9OPyrhrCSDv-KSYI-oRxgwkLSRheWksBoOobFmr2k9yeDTFfPQQA_xVchY2r1d2RUHB30cS2d9yNKI0DUyWYfycd36IIjPLqM-MDiYi4dUV9SKlvaCGHYtCuLL55MlT0ehtIJF8jqmMqmQ9BOlNhiZ3MGtg&topic=c600171ea687023a73a78c5bad2e01fae0497f6af8129a0334d1e3bd5e3030e3"
val envelope = "decrypted_envelope"

every { chaChaPolyCodec.decrypt(any(), any()) } returns envelope
coEvery { serializer.tryDeserialize<ClientJsonRpc>(any()) } returns clientJsonRpc
coEvery { serializer.tryDeserialize<JsonRpcResponse>(any()) } returns null
coEvery { serializer.tryDeserialize<JsonRpcResponse.JsonRpcError>(any()) } returns null

interactor.dispatchEnvelope(url)

coVerify {
chaChaPolyCodec.decrypt(Topic("c600171ea687023a73a78c5bad2e01fae0497f6af8129a0334d1e3bd5e3030e3"), any())
serializer.tryDeserialize<ClientJsonRpc>(envelope)
}
}

@Test
fun testDispatchEnvelopeWithMissingWc_evParameter() = testScope.runTest {
val url = "test_url?topic=test_topic"

try {
interactor.dispatchEnvelope(url)
} catch (e: IllegalStateException) {
assert(e.message == "LinkMode: Missing wc_ev parameter")
}
}

@Test
fun testDispatchEnvelopeWithMissingTopicParameter() = testScope.runTest {
val url = "test_url?wc_ev=encoded_envelope"

try {
interactor.dispatchEnvelope(url)
} catch (e: IllegalStateException) {
assert(e.message == "LinkMode: Missing topic parameter")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.walletconnect.android.di.overrideModule
import com.walletconnect.android.internal.common.crypto.kmr.KeyManagementRepository
import com.walletconnect.android.internal.common.di.AndroidCommonDITags
import com.walletconnect.android.internal.common.model.type.JsonRpcInteractorInterface
import com.walletconnect.android.internal.common.model.type.RelayJsonRpcInteractorInterface
import com.walletconnect.android.internal.common.wcKoinApp
import com.walletconnect.android.keyserver.domain.IdentitiesInteractor
import com.walletconnect.android.relay.ConnectionType
Expand Down Expand Up @@ -47,7 +48,7 @@ internal object TestClient {


internal val Relay get() = coreProtocol.Relay
internal val jsonRpcInteractor: JsonRpcInteractorInterface by lazy { wcKoinApp.koin.get() }
internal val jsonRpcInteractor: RelayJsonRpcInteractorInterface by lazy { wcKoinApp.koin.get() }
internal val keyManagementRepository: KeyManagementRepository by lazy { wcKoinApp.koin.get() }
internal val identitiesInteractor: IdentitiesInteractor by lazy { wcKoinApp.koin.get() }
internal val keyserverUrl: String by lazy { wcKoinApp.koin.get(named(AndroidCommonDITags.KEYSERVER_URL)) }
Expand Down Expand Up @@ -90,5 +91,8 @@ internal object TestClient {
}

internal val Relay get() = coreProtocol.Relay
internal val jsonRpcInteractor: RelayJsonRpcInteractorInterface by lazy { secondaryKoinApp.koin.get() }
internal val keyManagementRepository: KeyManagementRepository by lazy { secondaryKoinApp.koin.get() }

}
}
11 changes: 10 additions & 1 deletion core/android/src/main/kotlin/com/walletconnect/android/Core.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ object Core {
data class Error(val error: Throwable) : Ping()
}

data class AppMetaData(val name: String, val description: String, val url: String, val icons: List<String>, val redirect: String?, val verifyUrl: String? = null) : Model()
data class AppMetaData(
val name: String,
val description: String,
val url: String,
val icons: List<String>,
val redirect: String?,
val appLink: String? = null,
val linkMode: Boolean = false,
val verifyUrl: String? = null
) : Model()

data class DeletedPairing(val topic: String, val reason: String) : Model()
data class ExpiredPairing(val pairing: Pairing) : Model()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,19 @@ class CoreProtocol(private val koinApp: KoinApplication = wcKoinApp) : CoreInter
coreStorageModule(bundleId = bundleId),
pushModule(),
module { single { relay ?: Relay } },
module { single { with(metaData) { AppMetaData(name = name, description = description, url = url, icons = icons, redirect = Redirect(redirect)) } } },
module {
single {
with(metaData) {
AppMetaData(
name = name,
description = description,
url = url,
icons = icons,
redirect = Redirect(native = redirect, universal = appLink, linkMode = linkMode)
)
}
}
},
module { single { Echo } },
module { single { Push } },
module { single { Verify } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import com.walletconnect.util.randomBytes
import org.bouncycastle.crypto.modes.ChaCha20Poly1305
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
import org.bouncycastle.util.encoders.Base64
import java.nio.ByteBuffer

/* Note:
Expand All @@ -34,13 +33,14 @@ internal class ChaChaPolyCodec(private val keyManagementRepository: KeyManagemen
UnknownEnvelopeTypeException::class,
MissingParticipantsException::class
)
override fun encrypt(topic: Topic, payload: String, envelopeType: EnvelopeType, participants: Participants?): String {
override fun encrypt(topic: Topic, payload: String, envelopeType: EnvelopeType, participants: Participants?): ByteArray {
val input = payload.toByteArray(Charsets.UTF_8)
val nonceBytes = randomBytes(NONCE_SIZE)

return when (envelopeType.id) {
EnvelopeType.ZERO.id -> encryptEnvelopeType0(topic, nonceBytes, input, envelopeType)
EnvelopeType.ONE.id -> encryptEnvelopeType1(participants, nonceBytes, input, envelopeType)
EnvelopeType.TWO.id -> encryptEnvelopeType2(input, envelopeType)
else -> throw UnknownEnvelopeTypeException("Encrypt; Unknown envelope type: ${envelopeType.id}")
}
}
Expand All @@ -49,12 +49,11 @@ internal class ChaChaPolyCodec(private val keyManagementRepository: KeyManagemen
UnknownEnvelopeTypeException::class,
MissingKeyException::class
)
override fun decrypt(topic: Topic, cipherText: String): String {
val encryptedPayloadBytes = Base64.decode(cipherText)

return when (val envelopeType = encryptedPayloadBytes.envelopeType) {
EnvelopeType.ZERO.id -> decryptType0(topic, encryptedPayloadBytes)
EnvelopeType.ONE.id -> decryptType1(encryptedPayloadBytes, keyManagementRepository.getPublicKey(topic.getParticipantTag()))
override fun decrypt(topic: Topic, cipherText: ByteArray): String {
return when (val envelopeType = cipherText.envelopeType) {
EnvelopeType.ZERO.id -> decryptType0(topic, cipherText)
EnvelopeType.ONE.id -> decryptType1(cipherText, keyManagementRepository.getPublicKey(topic.getParticipantTag()))
EnvelopeType.TWO.id -> decryptType2(cipherText)
else -> throw UnknownEnvelopeTypeException("Decrypt; Unknown envelope type: $envelopeType")
}
}
Expand Down Expand Up @@ -98,7 +97,18 @@ internal class ChaChaPolyCodec(private val keyManagementRepository: KeyManagemen
return String(decryptedTextBytes, Charsets.UTF_8)
}

private fun encryptEnvelopeType0(topic: Topic, nonceBytes: ByteArray, input: ByteArray, envelopeType: EnvelopeType): String {
private fun decryptType2(encryptedPayloadBytes: ByteArray): String {
val envelopeType = ByteArray(ENVELOPE_TYPE_SIZE)
val encryptedMessageBytes = ByteArray(encryptedPayloadBytes.size - ENVELOPE_TYPE_SIZE)

val byteBuffer: ByteBuffer = ByteBuffer.wrap(encryptedPayloadBytes)
byteBuffer.get(envelopeType)
byteBuffer.get(encryptedMessageBytes)

return String(encryptedMessageBytes, Charsets.UTF_8)
}

private fun encryptEnvelopeType0(topic: Topic, nonceBytes: ByteArray, input: ByteArray, envelopeType: EnvelopeType): ByteArray {
val symmetricKey = keyManagementRepository.getSymmetricKey(topic.value)
val cipherBytes = encryptPayload(symmetricKey, nonceBytes, input)
val payloadSize = cipherBytes.size + NONCE_SIZE + ENVELOPE_TYPE_SIZE
Expand All @@ -108,15 +118,15 @@ internal class ChaChaPolyCodec(private val keyManagementRepository: KeyManagemen
.put(envelopeType.id).put(nonceBytes).put(cipherBytes)
.array()

return Base64.toBase64String(encryptedPayloadBytes)
return encryptedPayloadBytes
}

private fun encryptEnvelopeType1(
participants: Participants?,
nonceBytes: ByteArray,
input: ByteArray,
envelopeType: EnvelopeType,
): String {
): ByteArray {
if (participants == null) throw MissingParticipantsException("Missing participants when encrypting envelope type 1")
val self = participants.senderPublicKey
val selfBytes = self.keyAsHex.hexToBytes()
Expand All @@ -133,7 +143,20 @@ internal class ChaChaPolyCodec(private val keyManagementRepository: KeyManagemen
.put(cipherBytes)
.array()

return Base64.toBase64String(encryptedPayloadBytes)
return encryptedPayloadBytes
}

private fun encryptEnvelopeType2(
input: ByteArray,
envelopeType: EnvelopeType,
): ByteArray {
val payloadSize = input.size + ENVELOPE_TYPE_SIZE
val encryptedPayloadBytes = ByteBuffer.allocate(payloadSize)
.put(envelopeType.id)
.put(input)
.array()

return encryptedPayloadBytes
}

private fun encryptPayload(key: SymmetricKey, nonce: ByteArray, input: ByteArray): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import com.walletconnect.android.internal.common.model.Participants
import com.walletconnect.foundation.common.model.Topic

interface Codec {
fun encrypt(topic: Topic, payload: String, envelopeType: EnvelopeType, participants: Participants? = null): String
fun decrypt(topic: Topic, cipherText: String): String
fun encrypt(topic: Topic, payload: String, envelopeType: EnvelopeType, participants: Participants? = null): ByteArray
fun decrypt(topic: Topic, cipherText: ByteArray): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum class AndroidCommonDITags {
COLUMN_ADAPTER_LIST,
COLUMN_ADAPTER_MAP,
COLUMN_ADAPTER_APPMETADATATYPE,
COLUMN_ADAPTER_TRANSPORT_TYPE,
WEB3MODAL_URL,
WEB3MODAL_INTERCEPTOR,
WEB3MODAL_OKHTTP,
Expand Down
Loading

0 comments on commit a184ef9

Please sign in to comment.