Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix:inconsistency with ReCaps specs #1342

Merged
merged 13 commits into from
Mar 13, 2024
Merged
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
* @WalletConnect/kotlin-team @kacperoak @Elyniss @jakubuid @TalhaAli00
11 changes: 4 additions & 7 deletions .github/workflows/ci_db_migrations.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
name: DB Migration Verification

on:
push:
branches:
- develop
- master
pull_request:
branches:
- develop
- master
types:
- opened
- synchronize
- edited

concurrency:
# Support push/pr as event types with different behaviors each:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/ci_instrumented_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ concurrency:
jobs:
sdk_tests:
name: Run Instrumented Tests
runs-on:
group: apple-silicon
runs-on: macos-latest-xlarge
steps:
- uses: actions/checkout@v3
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.walletconnect.android.internal.common.cacao
package com.walletconnect.android

import com.walletconnect.android.BuildConfig
import com.walletconnect.android.cacao.signature.SignatureType
import com.walletconnect.android.internal.common.model.ProjectId
import com.walletconnect.android.internal.common.signing.cacao.Cacao
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.walletconnect.android.internal.common.cacao
package com.walletconnect.android.test

import com.walletconnect.android.internal.common.signing.cacao.Cacao
import com.walletconnect.android.internal.common.signing.cacao.toCAIP222Message
Expand All @@ -14,7 +14,7 @@ internal class MapperTest {
"urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvcGVyc29uYWxfc2lnbiI6W3t9XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YV92NCI6W3t9XX0sImh0dHBzOi8vbm90aWZ5LndhbGxldGNvbm5lY3QuY29tL2FsbC1hcHBzIjp7ImNydWQvc3Vic2NyaXB0aW9ucyI6W3t9XSwiY3J1ZC9ub3RpZmljYXRpb25zIjpbe31dfX19"

@Test
fun `Payload required fields formatting`() {
fun payloadRequiredFieldsFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -44,7 +44,7 @@ internal class MapperTest {
}

@Test
fun `Test formatting CAIP-222 message with Sign ReCaps`() {
fun testFormattingCAIP222MessageWithSignReCaps() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -82,7 +82,7 @@ internal class MapperTest {
}

@Test
fun `Test formatting CAIP-222 message with Sign Without statement`() {
fun testFormattingCAIP222MessageWithSignWithoutStatement() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -120,7 +120,7 @@ internal class MapperTest {
}

@Test
fun `Test formatting CAIP-222 message with Notify ReCaps`() {
fun testFormattingCAIP222MessageWithNotifyReCaps() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -155,7 +155,7 @@ internal class MapperTest {
}

@Test
fun `Test formatting CAIP-222 message with Notify ReCaps and Sign ReCaps`() {
fun testFormattingCAIP222MessageWithNotifyReCapsAndSignReCaps() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand All @@ -178,7 +178,7 @@ internal class MapperTest {
val message = "service.invalid wants you to sign in with your Ethereum account:\n" +
"0x15bca56b6e2728aec2532df9d436bd1600e86688\n" +
"\n" +
"Statement I further authorize the stated URI to perform the following actions on my behalf: (1) 'crud': 'notifications', 'subscriptions' for 'https://notify.walletconnect.com/all-apps'. (2) 'request': 'eth_signTypedData_v4', 'personal_sign' for 'eip155'.\n" +
"Statement I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_signTypedData_v4', 'personal_sign' for 'eip155'.\n" +
"\n" +
"URI: https://service.invalid/login\n" +
"Version: 1\n" +
Expand All @@ -195,7 +195,7 @@ internal class MapperTest {
}

@Test
fun `Test formatting CAIP-222 message with Notify ReCaps and Sign ReCaps in one URN`() {
fun testFormattingCAIP222MessageWithNotifyReCapsAndSignReCapsInOneURN() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -233,7 +233,7 @@ internal class MapperTest {
}

@Test
fun `Payload resources formatting`() {
fun payloadResourcesFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -271,7 +271,7 @@ internal class MapperTest {
}

@Test
fun `Payload requestId formatting`() {
fun payloadRequestIdFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -301,7 +301,7 @@ internal class MapperTest {
}

@Test
fun `Payload statement formatting`() {
fun payloadStatementFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -331,7 +331,7 @@ internal class MapperTest {
}

@Test
fun `Payload expiry formatting`() {
fun payloadExpiryFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -361,7 +361,7 @@ internal class MapperTest {
}

@Test
fun `Payload not before formatting`() {
fun payloadNotBeforeFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down Expand Up @@ -391,7 +391,7 @@ internal class MapperTest {
}

@Test
fun `Payload all fields formatting`() {
fun payloadAllFieldsFormatting() {
val payload = Cacao.Payload(
iss = iss,
domain = "service.invalid",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ data class Cacao(
val resources: List<String>?,
) {
@get:Throws(Exception::class)
val actionsString get() = getActionsString()
val actionsString get() = resources.getActionsString()

@get:Throws(Exception::class)
val methods get() = resources.getMethods()
Expand All @@ -78,12 +78,16 @@ internal fun Cacao.Signature.toSignature(): Signature = Signature.fromString(s)

fun Cacao.Payload.toCAIP222Message(chainName: String = "Ethereum"): String {
var message = "$domain wants you to sign in with your $chainName account:\n${Issuer(iss).address}\n\n"
if (statement != null) message += "$statement"
if (resources?.find { r -> r.startsWith(RECAPS_PREFIX) } != null) {
message += if (statement != null) " " else ""
message += "I further authorize the stated URI to perform the following actions on my behalf: $actionsString.\n"
} else if (statement != null) {
message += "\n"
if (statement?.contains(RECAPS_STATEMENT) == true) {
message += "$statement\n"
} else {
if (statement != null) message += "$statement"
if (resources?.find { r -> r.startsWith(RECAPS_PREFIX) } != null) {
message += if (statement != null) " " else ""
message += "$RECAPS_STATEMENT: ${resources.getActionsString()}.\n"
} else if (statement != null) {
message += "\n"
}
}
message += "\nURI: $aud\nVersion: $version\nChain ID: ${Issuer(iss).chainIdReference}\nNonce: $nonce\nIssued At: $iat"
if (exp != null) message += "\nExpiration Time: $exp"
Expand All @@ -97,8 +101,20 @@ fun Cacao.Payload.toCAIP222Message(chainName: String = "Ethereum"): String {
return message
}

private fun Cacao.Payload.getActionsString(): String {
val map = resources.decodeReCaps().parseReCaps()
fun Pair<String?, List<String>?>.getStatement(): String {
val (statement, resources) = this
var newStatement = ""
if (statement != null) newStatement += "$statement"
if (resources?.find { r -> r.startsWith(RECAPS_PREFIX) } != null) {
newStatement += if (statement != null) " " else ""
newStatement += "$RECAPS_STATEMENT: ${resources.getActionsString()}."
}

return newStatement
}

private fun List<String>?.getActionsString(): String {
val map = this.decodeReCaps().parseReCaps()
if (map.isEmpty()) throw Exception("Decoded ReCaps map is empty")
var result = ""
var index = 1
Expand All @@ -116,4 +132,6 @@ private fun Cacao.Payload.getActionsString(): String {
}

return result
}
}

const val RECAPS_STATEMENT: String = "I further authorize the stated URI to perform the following actions on my behalf"
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class CacaoVerifier(private val projectId: ProjectId) {

SignatureType.EIP191.header, SignatureType.EIP1271.header -> {
val plainMessage = cacao.payload.toCAIP222Message()
println(plainMessage)
val hexMessage = Numeric.toHexString(cacao.payload.toCAIP222Message().toByteArray())
val address = Issuer(cacao.payload.iss).address

Expand Down
Original file line number Diff line number Diff line change
@@ -1,56 +1,62 @@
package com.walletconnect.android.internal.common.signing.cacao

import android.util.Base64
import com.walletconnect.android.internal.common.signing.cacao.Cacao.Payload.Companion.RECAPS_PREFIX
import com.walletconnect.utils.HexPrefix
import org.bouncycastle.util.encoders.Base64
import org.json.JSONArray
import org.json.JSONObject

@JvmSynthetic
internal fun String.guaranteeNoHexPrefix(): String = removePrefix(String.HexPrefix)

@JvmSynthetic
fun List<String>?.parseReCaps(): MutableMap<String, MutableMap<String, MutableList<String>>> {
fun String?.parseReCaps(): MutableMap<String, MutableMap<String, MutableList<String>>> {
if (this.isNullOrEmpty()) return emptyMap<String, MutableMap<String, MutableList<String>>>().toMutableMap()
val reCapsMap: MutableMap<String, MutableMap<String, MutableList<String>>> = mutableMapOf()
this.forEach { jsonString ->
val jsonObject = JSONObject(jsonString)
val attObject = jsonObject.getJSONObject("att")

attObject.keys().forEach { key ->
val innerObject = attObject.getJSONObject(key)
val requestsMap = mutableMapOf<String, MutableList<String>>()

innerObject.keys().forEach { requestType ->
val requestArray = innerObject.getJSONArray(requestType)
val dynamicList = mutableListOf<String>()

for (i in 0 until requestArray.length()) {
val itemObject = requestArray.getJSONObject(i)
// Assuming the structure under each requestType contains arrays of strings
itemObject.keys().forEach { dynamicKey ->
val dynamicArray = itemObject.getJSONArray(dynamicKey)
for (j in 0 until dynamicArray.length()) {
dynamicList.add(dynamicArray.getString(j))
}

val jsonObject = JSONObject(this)
val attObject = jsonObject.getJSONObject("att")

attObject.keys().forEach { key ->
val innerObject = attObject.getJSONObject(key)
val requestsMap = mutableMapOf<String, MutableList<String>>()

innerObject.keys().forEach { requestType ->
val requestArray = innerObject.getJSONArray(requestType)
val dynamicList = mutableListOf<String>()

for (i in 0 until requestArray.length()) {
val itemObject = requestArray.getJSONObject(i)
// Assuming the structure under each requestType contains arrays of strings
itemObject.keys().forEach { dynamicKey ->
val dynamicArray = itemObject.getJSONArray(dynamicKey)
for (j in 0 until dynamicArray.length()) {
dynamicList.add(dynamicArray.getString(j))
}
}

requestsMap[requestType] = dynamicList
}

reCapsMap[key] = requestsMap
requestsMap[requestType] = dynamicList
}

reCapsMap[key] = requestsMap
}

return reCapsMap.mapValues { entry -> entry.value.toMutableMap() }.toMutableMap()
}

@JvmSynthetic
fun List<String>?.decodeReCaps(): List<String>? {
return this
?.filter { resource -> resource.startsWith(RECAPS_PREFIX) }
?.map { urn -> urn.removePrefix(RECAPS_PREFIX) }
?.map { encodedReCaps -> Base64.decode(encodedReCaps).toString(Charsets.UTF_8) }
fun List<String>?.decodeReCaps(): String? {
return try {
val last = this?.last()
if (last != null && last.startsWith(RECAPS_PREFIX)) {
Base64.decode(last.removePrefix(RECAPS_PREFIX), Base64.NO_WRAP).toString(Charsets.UTF_8)
} else {
null
}
} catch (e: Exception) {
null
}
}

@JvmSynthetic
Expand All @@ -61,4 +67,49 @@ fun List<String>?.getMethods(): List<String> {
@JvmSynthetic
fun List<String>?.getChains(): List<String> {
return this.decodeReCaps().parseReCaps()["eip155"]?.values?.flatten()?.distinct() ?: emptyList()
}

@JvmSynthetic
fun mergeReCaps(json1: JSONObject, json2: JSONObject): String {
val result = JSONObject(json1.toString()) // Start with a deep copy of json1

json2.keys().forEach { key ->
if (!result.has(key)) {
// If json1 does not have the key, simply put the json2 object/array/primitive
result.put(key, json2.get(key))
} else {
// If both json1 and json2 have the object, merge them
val value1 = result.get(key)
val value2 = json2.get(key)

when {
value1 is JSONObject && value2 is JSONObject -> {
result.put(key, mergeReCaps(value1, value2))
}

value1 is JSONArray && value2 is JSONArray -> {
// Concatenate arrays, respecting ordering rules if specified
val mergedArray = concatenateJsonArrays(value1, value2)
result.put(key, mergedArray)
}

else -> {
// For primitive types or if types are different, json2 overrides json1
result.put(key, value2)
}
}
}
}
return result.toString().replace("\\\"", "\"").replace("\"{", "{").replace("}\"", "}")
}

private fun concatenateJsonArrays(arr1: JSONArray, arr2: JSONArray): JSONArray {
val result = JSONArray()
for (i in 0 until arr1.length()) {
result.put(arr1.get(i))
}
for (i in 0 until arr2.length()) {
result.put(arr2.get(i))
}
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ object Web3Wallet {
return com.walletconnect.sign.client.utils.generateAuthPayloadParams(payloadParams.toSign(), supportedChains, supportedMethods).toWallet()
}


@Throws(IllegalStateException::class)
fun updateSession(
params: Wallet.Params.SessionUpdate,
Expand Down
Loading
Loading