Skip to content

Commit

Permalink
Validating consent proof
Browse files Browse the repository at this point in the history
Added validation for consent proof
Added additional type updates
Added key utils
Added new signing message
  • Loading branch information
Alex Risch authored and Alex Risch committed Apr 23, 2024
1 parent 7a54600 commit cf775ad
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.xmtp.android.library.codecs.TextCodec
Expand All @@ -16,11 +18,15 @@ import org.xmtp.android.library.messages.MessageBuilder
import org.xmtp.android.library.messages.MessageV1Builder
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.SealedInvitationBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.messages.Topic
import org.xmtp.android.library.messages.consentProofText
import org.xmtp.android.library.messages.createDeterministic
import org.xmtp.android.library.messages.getPublicKeyBundle
import org.xmtp.android.library.messages.toPublicKeyBundle
import org.xmtp.android.library.messages.walletAddress
import org.xmtp.proto.message.contents.Invitation
import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload
import java.lang.Thread.sleep
import java.util.Date

Expand Down Expand Up @@ -182,21 +188,82 @@ class ConversationsTest {
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
val boClient = Client().create(bo, clientOptions)
val alixClient = Client().create(alix, clientOptions)
val signedBytes = byteArrayOf(1, 2, 3)
val privateKeyBundle = alixClient.keys
val signedPrivateKey = privateKeyBundle.identityKey
val signature = runBlocking {
val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey)
PrivateKeyBuilder(privateKey).sign(signedBytes)
}
val timestamp = System.currentTimeMillis()
val signatureText = Signature.newBuilder().build().consentProofText(boClient.address, timestamp)
val digest = signatureText.toByteArray()
val signature = runBlocking { alix.sign(Util.keccak256(digest)) }
val hex = signature.toByteArray().toHex()
val consentProofPayload = ConsentProofPayload.newBuilder().also {
it.signature = hex
it.timestamp = timestamp
it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1
}.build()
val boConversation =
runBlocking { boClient.conversations.newConversation(alixClient.address, null, signature.toByteArray().toHex()) }
runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) }
val alixConversations = runBlocking {
alixClient.conversations.list()
}
val alixConversation = alixConversations.find {
it.topic == boConversation.topic
}
assertNotNull(alixConversation)
val isAllowed = runBlocking { alixClient.contacts.isAllowed(boClient.address) }
assertTrue(isAllowed)
}

@Test
fun testNetworkConsentOverConsentProof() {
val bo = PrivateKeyBuilder()
val alix = PrivateKeyBuilder()
val clientOptions =
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
val boClient = Client().create(bo, clientOptions)
val alixClient = Client().create(alix, clientOptions)
val timestamp = System.currentTimeMillis()
val signatureText = Signature.newBuilder().build().consentProofText(boClient.address, timestamp)
val digest = signatureText.toByteArray()

val signature = runBlocking { alix.sign(Util.keccak256(digest)) }
val hex = signature.toByteArray().toHex()
val consentProofPayload = ConsentProofPayload.newBuilder().also {
it.signature = hex
it.timestamp = timestamp
it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1
}.build()
runBlocking { alixClient.contacts.deny(listOf(boClient.address)) }
val boConversation = runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) }
val alixConversations = runBlocking { alixClient.conversations.list() }
val alixConversation = alixConversations.find { it.topic == boConversation.topic }
assertNotNull(alixConversation)
val isDenied = runBlocking { alixClient.contacts.isDenied(boClient.address) }
assertTrue(isDenied)
}

@Test
fun testConsentProofInvalidSignature() {
val bo = PrivateKeyBuilder()
val alix = PrivateKeyBuilder()
val clientOptions =
ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false))
val boClient = Client().create(bo, clientOptions)
val alixClient = Client().create(alix, clientOptions)
val timestamp = System.currentTimeMillis()
val signatureText = Signature.newBuilder().build().consentProofText(boClient.address, timestamp + 1)
val digest = signatureText.toByteArray()

val signature = runBlocking { alix.sign(Util.keccak256(digest)) }
val hex = signature.toByteArray().toHex()
val consentProofPayload = ConsentProofPayload.newBuilder().also {
it.signature = hex
it.timestamp = timestamp
it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1
}.build()

val boConversation = runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) }
val alixConversations = runBlocking { alixClient.conversations.list() }
val alixConversation = alixConversations.find { it.topic == boConversation.topic }
assertNotNull(alixConversation)
val isAllowed = runBlocking { alixClient.contacts.isAllowed(boClient.address) }
assertFalse(isAllowed)
}
}
10 changes: 10 additions & 0 deletions library/src/main/java/org/xmtp/android/library/Conversation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.xmtp.android.library.messages.PagingInfoSortDirection
import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData
import org.xmtp.proto.message.api.v1.MessageApiOuterClass
import org.xmtp.proto.message.contents.Invitation
import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload
import org.xmtp.proto.message.contents.Invitation.InvitationV1.Aes256gcmHkdfsha256
import java.util.Date

Expand Down Expand Up @@ -288,6 +289,15 @@ sealed class Conversation {
}
}

val consentProof: ConsentProofPayload?
get() {
return when (this) {
is V1 -> return null
is V2 -> conversationV2.consentProof
is Group -> return null
}
}

// Get the client according to the version
val client: Client
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ data class ConversationV2(
val topic: String,
val keyMaterial: ByteArray,
val context: Invitation.InvitationV1.Context,
var consentProof: Invitation.ConsentProofPayload? = null,
val peerAddress: String,
val client: Client,
val createdAtNs: Long? = null,
Expand All @@ -52,6 +53,7 @@ data class ConversationV2(
client = client,
createdAtNs = header.createdNs,
header = header,
consentProof = if (invitation.hasConsentProof()) invitation.consentProof else null
)
}
}
Expand Down
45 changes: 40 additions & 5 deletions library/src/main/java/org/xmtp/android/library/Conversations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.runBlocking
import org.web3j.utils.Numeric
import org.xmtp.android.library.GRPCApiClient.Companion.makeQueryRequest
import org.xmtp.android.library.GRPCApiClient.Companion.makeSubscribeRequest
import org.xmtp.android.library.libxmtp.MessageV3
Expand All @@ -21,13 +23,16 @@ import org.xmtp.android.library.messages.MessageV1Builder
import org.xmtp.android.library.messages.Pagination
import org.xmtp.android.library.messages.SealedInvitation
import org.xmtp.android.library.messages.SealedInvitationBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.messages.SignedPublicKeyBundle
import org.xmtp.android.library.messages.Topic
import org.xmtp.android.library.messages.consentProofText
import org.xmtp.android.library.messages.createDeterministic
import org.xmtp.android.library.messages.decrypt
import org.xmtp.android.library.messages.getInvitation
import org.xmtp.android.library.messages.header
import org.xmtp.android.library.messages.involves
import org.xmtp.android.library.messages.rawData
import org.xmtp.android.library.messages.recipientAddress
import org.xmtp.android.library.messages.senderAddress
import org.xmtp.android.library.messages.sentAt
Expand Down Expand Up @@ -148,6 +153,31 @@ data class Conversations(
} ?: emptyList()
}

private fun validateConsentSignature(signature: String, clientAddress: String, peerAddress: String, timestamp: Long): Boolean {
val messageData = Signature.newBuilder().build().consentProofText(peerAddress, timestamp).toByteArray()
val signatureData = Numeric.hexStringToByteArray(signature)
val sig = Signature.parseFrom(signatureData)
val recoveredPublicKey = KeyUtil.recoverPublicKeyKeccak256(sig.rawData.toByteString().toByteArray(), Util.keccak256(messageData))
?: return false
return clientAddress == KeyUtil.publicKeyToAddress(recoveredPublicKey)
}

private fun handleConsentProof(consentProof: Invitation.ConsentProofPayload, peerAddress: String) {
val signature = consentProof.signature
val timestamp = consentProof.timestamp

if (!validateConsentSignature(signature, client.address, peerAddress, timestamp)) {
return
}
val contacts = client.contacts
runBlocking {
contacts.refreshConsentList()
if (contacts.consentList.state(peerAddress) == ConsentState.UNKNOWN) {
contacts.allow(listOf(peerAddress))
}
}
}

/**
* This creates a new [Conversation] using a specified address
* @param peerAddress The address of the client that you want to start a new conversation
Expand All @@ -158,7 +188,7 @@ data class Conversations(
suspend fun newConversation(
peerAddress: String,
context: Invitation.InvitationV1.Context? = null,
consentProofSignature: String? = null,
consentProof: Invitation.ConsentProofPayload? = null,
): Conversation {
if (peerAddress.lowercase() == client.address.lowercase()) {
throw XMTPException("Recipient is sender")
Expand Down Expand Up @@ -215,6 +245,7 @@ data class Conversations(
peerAddress = peerAddress,
client = client,
header = sealedInvitation.v1.header,
consentProof = if (invite.hasConsentProof()) invite.consentProof else null
),
)
conversationsByTopic[conversation.topic] = conversation
Expand All @@ -224,7 +255,7 @@ data class Conversations(
// We don't have an existing conversation, make a v2 one
val recipient = contact.toSignedPublicKeyBundle()
val invitation = Invitation.InvitationV1.newBuilder().build()
.createDeterministic(client.keys, recipient, context, consentProofSignature)
.createDeterministic(client.keys, recipient, context, consentProof)
val sealedInvitation =
sendInvitation(recipient = recipient, invitation = invitation, created = Date())
val conversationV2 = ConversationV2.create(
Expand Down Expand Up @@ -261,9 +292,12 @@ data class Conversations(
val invitations = listInvitations(pagination = pagination)
for (sealedInvitation in invitations) {
try {
// TODO: Verify Signature and approve
newConversations.add(Conversation.V2(conversation(sealedInvitation)))
if (sealedInvitation.)
val newConversation = Conversation.V2(conversation(sealedInvitation))
newConversations.add(newConversation)
val consentProof = newConversation.consentProof
if (consentProof != null) {
handleConsentProof(consentProof, newConversation.peerAddress)
}
} catch (e: Exception) {
Log.d(TAG, e.message.toString())
}
Expand Down Expand Up @@ -302,6 +336,7 @@ data class Conversations(
client = client,
createdAtNs = data.createdNs,
header = Invitation.SealedInvitationHeaderV1.getDefaultInstance(),
consentProof = if (data.invitation.hasConsentProof()) data.invitation.consentProof else null
),
)
}
Expand Down
15 changes: 15 additions & 0 deletions library/src/main/java/org/xmtp/android/library/KeyUtil.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.xmtp.android.library

import org.web3j.crypto.ECDSASignature
import org.web3j.crypto.Keys
import org.web3j.crypto.Sign
import org.web3j.crypto.Sign.SignatureData
import java.math.BigInteger
Expand All @@ -9,6 +11,19 @@ object KeyUtil {
return Sign.publicKeyFromPrivate(BigInteger(1, privateKey)).toByteArray()
}

fun recoverPublicKeyKeccak256(signature: ByteArray, digest: ByteArray): BigInteger? {
val signatureData = getSignatureData(signature)
return Sign.recoverFromSignature(
BigInteger(1, signatureData.v).toInt(),
ECDSASignature(BigInteger(1, signatureData.r), BigInteger(1, signatureData.s)),
digest,
)
}

fun publicKeyToAddress(publicKey: BigInteger): String {
return Keys.toChecksumAddress(Keys.getAddress(publicKey))
}

fun addUncompressedByte(publicKey: ByteArray): ByteArray {
return if (publicKey.size >= 65) {
val newPublicKey = ByteArray(64)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ fun InvitationV1.createDeterministic(
sender: PrivateKeyBundleV2,
recipient: SignedPublicKeyBundle,
context: Context? = null,
consentProofSignature: String? = null
consentProof: ConsentProofPayload? = null
): InvitationV1 {
val myAddress = sender.toV1().walletAddress
val theirAddress = recipient.walletAddress
Expand Down Expand Up @@ -98,12 +98,6 @@ fun InvitationV1.createDeterministic(
this.keyMaterial = keyMaterial.toByteString()
}.build()

val consentProof = ConsentProofPayload.newBuilder().apply {
this.signature = consentProofSignature
this.timestamp = Date().time * 1_000_000
this.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1
}.build()

return InvitationV1Builder.buildFromTopic(
topic = topic,
context = inviteContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ fun Signature.createIdentityText(key: ByteArray): String =
fun Signature.enableIdentityText(key: ByteArray): String =
("XMTP : Enable Identity\n" + "${key.toHex()}\n" + "\n" + "For more info: https://xmtp.org/signatures/")

fun Signature.consentProofText(peerAddress: String, timestamp: Long): String =
("XMTP : Grant inbox consent to sender\n" + "\n" + "Current Time: ${timestamp}\n" + "From Address: ${peerAddress}\n" + "\n" + "For more info: https://xmtp.org/signatures/")

val Signature.rawData: ByteArray
get() = if (hasEcdsaCompact()) {
ecdsaCompact.bytes.toByteArray() + ecdsaCompact.recovery.toByte()
Expand Down

0 comments on commit cf775ad

Please sign in to comment.