diff --git a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt index 98c287ed6..1f0086f8f 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt @@ -5,7 +5,6 @@ import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.xmtp.android.library.messages.PrivateKeyBuilder @@ -202,7 +201,6 @@ class ClientTest { } @Test - @Ignore("CI Issues") fun testPublicCanMessage() { val aliceWallet = PrivateKeyBuilder() val notOnNetwork = PrivateKeyBuilder() @@ -218,7 +216,6 @@ class ClientTest { } @Test - @Ignore("CI Issues") fun testPreEnableIdentityCallback() { val fakeWallet = PrivateKeyBuilder() val expectation = CompletableFuture() @@ -241,7 +238,6 @@ class ClientTest { } @Test - @Ignore("CI Issues") fun testPreCreateIdentityCallback() { val fakeWallet = PrivateKeyBuilder() val expectation = CompletableFuture() diff --git a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt index 4b04fe815..3d7464344 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt @@ -3,8 +3,10 @@ package org.xmtp.android.library import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.protobuf.kotlin.toByteStringUtf8 import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.xmtp.android.library.Crypto.Companion.verifyHmacSignature import org.xmtp.android.library.codecs.CompositeCodec import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.ContentTypeId @@ -12,15 +14,19 @@ import org.xmtp.android.library.codecs.ContentTypeIdBuilder import org.xmtp.android.library.codecs.DecodedComposite import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.TextCodec +import org.xmtp.android.library.messages.InvitationV1ContextBuilder +import org.xmtp.android.library.messages.MessageV2Builder +import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress +import java.time.Instant data class NumberCodec( override var contentType: ContentTypeId = ContentTypeIdBuilder.builderFromAuthorityId( authorityId = "example.com", typeId = "number", versionMajor = 1, - versionMinor = 1 - ) + versionMinor = 1, + ), ) : ContentCodec { override fun encode(content: Double): EncodedContent { return EncodedContent.newBuilder().also { @@ -28,7 +34,7 @@ data class NumberCodec( authorityId = "example.com", typeId = "number", versionMajor = 1, - versionMinor = 1 + versionMinor = 1, ) it.content = mapOf(Pair("number", content)).toString().toByteStringUtf8() }.build() @@ -37,10 +43,13 @@ data class NumberCodec( override fun decode(content: EncodedContent): Double = content.content.toStringUtf8().filter { it.isDigit() || it == '.' }.toDouble() + override fun shouldPush(content: Double): Boolean = false + override fun fallback(content: Double): String? { return "Error: This app does not support numbers." } } + @RunWith(AndroidJUnit4::class) class CodecTest { @@ -53,7 +62,7 @@ class CodecTest { aliceClient.conversations.newConversation(fixtures.bob.walletAddress) aliceConversation.send( content = 3.14, - options = SendOptions(contentType = NumberCodec().contentType) + options = SendOptions(contentType = NumberCodec().contentType), ) val messages = aliceConversation.messages() assertEquals(messages.size, 1) @@ -75,7 +84,7 @@ class CodecTest { val source = DecodedComposite(encodedContent = textContent) aliceConversation.send( content = source, - options = SendOptions(contentType = CompositeCodec().contentType) + options = SendOptions(contentType = CompositeCodec().contentType), ) val messages = aliceConversation.messages() val decoded: DecodedComposite? = messages[0].content() @@ -95,12 +104,12 @@ class CodecTest { val source = DecodedComposite( parts = listOf( DecodedComposite(encodedContent = textContent), - DecodedComposite(parts = listOf(DecodedComposite(encodedContent = numberContent))) - ) + DecodedComposite(parts = listOf(DecodedComposite(encodedContent = numberContent))), + ), ) aliceConversation.send( content = source, - options = SendOptions(contentType = CompositeCodec().contentType) + options = SendOptions(contentType = CompositeCodec().contentType), ) val messages = aliceConversation.messages() val decoded: DecodedComposite? = messages[0].content() @@ -109,4 +118,93 @@ class CodecTest { assertEquals("sup", part1.content()) assertEquals(3.14, part2.content()) } + + @Test + fun testCanGetPushInfoBeforeDecoded() { + val codec = NumberCodec() + Client.register(codec = codec) + val fixtures = fixtures() + val aliceClient = fixtures.aliceClient!! + val aliceConversation = + aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + aliceConversation.send( + content = 3.14, + options = SendOptions(contentType = codec.contentType), + ) + val messages = aliceConversation.messages() + assert(messages.isNotEmpty()) + + val message = MessageV2Builder.buildEncode( + client = aliceClient, + encodedContent = messages[0].encodedContent, + topic = aliceConversation.topic, + keyMaterial = aliceConversation.keyMaterial!!, + codec = codec, + ) + + assertEquals(false, message.shouldPush) + assertEquals(true, message.senderHmac?.isNotEmpty()) + } + + @Test + fun testReturnsAllHMACKeys() { + val alix = PrivateKeyBuilder() + val clientOptions = + ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) + val alixClient = Client().create(alix, clientOptions) + val conversations = mutableListOf() + repeat(5) { + val account = PrivateKeyBuilder() + val client = Client().create(account, clientOptions) + conversations.add( + alixClient.conversations.newConversation( + client.address, + context = InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi") + ) + ) + } + + val thirtyDayPeriodsSinceEpoch = Instant.now().epochSecond / 60 / 60 / 24 / 30 + + val hmacKeys = alixClient.conversations.getHmacKeys() + + val topics = hmacKeys.hmacKeysMap.keys + conversations.forEach { convo -> + assertTrue(topics.contains(convo.topic)) + } + + val topicHmacs = mutableMapOf() + val headerBytes = ByteArray(10) + + conversations.forEach { conversation -> + val topic = conversation.topic + val payload = TextCodec().encode(content = "Hello, world!") + + val message = MessageV2Builder.buildEncode( + client = alixClient, + encodedContent = payload, + topic = topic, + keyMaterial = headerBytes, + codec = TextCodec() + ) + + val keyMaterial = conversation.keyMaterial + val info = "$thirtyDayPeriodsSinceEpoch-${alixClient.address}" + val key = Crypto.deriveKey(keyMaterial!!, ByteArray(0), info.toByteArray()) + val hmac = Crypto.calculateMac(key, headerBytes) + + topicHmacs[topic] = hmac + } + + hmacKeys.hmacKeysMap.forEach { (topic, hmacData) -> + hmacData.valuesList.forEachIndexed { idx, hmacKeyThirtyDayPeriod -> + val valid = verifyHmacSignature( + hmacKeyThirtyDayPeriod.hmacKey.toByteArray(), + topicHmacs[topic]!!, + headerBytes + ) + assertTrue(valid == (idx == 1)) + } + } + } } diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt index 7dde5004c..0b0e87155 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt @@ -229,11 +229,16 @@ class ConversationTest { additionalData = headerBytes, ) val tamperedMessage = - MessageV2Builder.buildFromCipherText(headerBytes = headerBytes, ciphertext = ciphertext) + MessageV2Builder.buildFromCipherText( + headerBytes = headerBytes, + ciphertext = ciphertext, + senderHmac = null, + shouldPush = true, + ) val tamperedEnvelope = EnvelopeBuilder.buildFromString( topic = aliceConversation.topic, timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = tamperedMessage).toByteArray(), + message = MessageBuilder.buildFromMessageV2(v2 = tamperedMessage.messageV2).toByteArray(), ) aliceClient.publish(envelopes = listOf(tamperedEnvelope)) val bobConversation = bobClient.conversations.newConversation( @@ -585,7 +590,8 @@ class ConversationTest { encodedContent, topic = conversation.topic, keyMaterial = conversation.keyMaterial!!, - ), + codec = encoder, + ).messageV2, ).toByteArray(), ), ) @@ -848,7 +854,6 @@ class ConversationTest { val directMessageV1 = Topic.directMessageV1(invalidId, "sd").description val directMessageV2 = Topic.directMessageV2(invalidId).description val preferenceList = Topic.preferenceList(invalidId).description - val conversations = bobClient.conversations // check if validation of topics no accept all types with invalid topic assertFalse(Topic.isValidTopic(privateStore)) diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupMembershipChangeTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupMembershipChangeTest.kt index 96f53d7b0..8cb1e9de1 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupMembershipChangeTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupMembershipChangeTest.kt @@ -1,7 +1,8 @@ package org.xmtp.android.library +import android.content.Context +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -25,18 +26,17 @@ class GroupMembershipChangeTest { lateinit var caro: PrivateKey lateinit var caroClient: Client lateinit var fixtures: Fixtures + val context = ApplicationProvider.getApplicationContext() @Before fun setUp() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - fixtures = - fixtures( - clientOptions = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableAlphaMls = true, - appContext = context - ) + fixtures = fixtures( + clientOptions = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, false), + enableAlphaMls = true, + appContext = context, ) + ) alixWallet = fixtures.aliceAccount alix = fixtures.alice boWallet = fixtures.bobAccount diff --git a/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt b/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt index 171377d17..d89455406 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt @@ -53,7 +53,7 @@ class MessageTest { sender = alice, recipient = bob.toPublicKeyBundle(), message = content, - timestamp = Date() + timestamp = Date(), ) assertEquals(aliceWallet.getPrivateKey().walletAddress, message1.senderAddress) assertEquals(bobWallet.getPrivateKey().walletAddress, message1.recipientAddress) @@ -77,13 +77,13 @@ class MessageTest { InvitationV1.newBuilder().build().createDeterministic( sender = alice.toV2(), recipient = bob.toV2().getPublicKeyBundle(), - context = invitationContext + context = invitationContext, ) val sealedInvitation = SealedInvitationBuilder.buildFromV1( sender = alice.toV2(), recipient = bob.toV2().getPublicKeyBundle(), created = Date(), - invitation = invitationv1 + invitation = invitationv1, ) val encoder = TextCodec() val encodedContent = encoder.encode(content = "Yo!") @@ -91,14 +91,15 @@ class MessageTest { client = client, encodedContent, topic = invitationv1.topic, - keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray() + keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray(), + codec = encoder, ) val decoded = MessageV2Builder.buildDecode( id = "", client = client, - message = message1, + message = message1.messageV2, keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray(), - topic = invitationv1.topic + topic = invitationv1.topic, ) val result: String? = decoded.content() assertEquals(result, "Yo!") @@ -118,7 +119,7 @@ class MessageTest { Numeric.hexStringToByteArray("d752fb09ee0390fe5902a1bd7b2f530da7e5b3a2bd91bad9df8fa284ab63327b86a59620fd3e2d2cf9183f46bd0fe75bda3caca893420c38416b1f") val additionalData = Numeric.hexStringToByteArray( - "0aac020a940108d995eeadcc3012460a440a408f20c9fc03909edeb21538b0a568c423f8829e95c0270779ca704f72a45f02416f6071f6faaf421cac3bacc6bb432fc4b5f92bc4391349953c7c98f12253cdd710011a430a4104b7eb7b56059a4f08bf3dd8f1b329e21d486e39822f17db15bad0d7f689f6c8081ae2800b9014fc9ef355a39e10503fddfdfa0b07ccc1946c2275b10e660d5ded12920108e995eeadcc3012440a420a40da669aa014468ffe34d5b962443d8b1e353b1e39f252bbcffa5c6c70adf9f7d2484de944213f345bac869e8c1942657b9c59f6fc12d139171b22789bc76ffb971a430a4104901d3a7f728bde1f871bcf46d44dcf34eead4c532135913583268d35bd93ca0a1571a8cb6546ab333f2d77c3bb9839be7e8f27795ea4d6e979b6670dec20636d12aa020a920108bad3eaadcc3012440a420a4016d83a6e44ee8b9764f18fbb390f2a4049d92ff904ebd75c76a71d58a7f943744f8bed7d3696f9fb41ce450c5ab9f4a7f9a83e3d10f401bbe85e3992c5156d491a430a41047cebe3a23e573672363665d13220d368d37776e10232de9bd382d5af36392956dbd806f8b78bec5cdc111763e4ef4aff7dee65a8a15fee8d338c387320c5b23912920108bad3eaadcc3012440a420a404a751f28001f34a4136529a99e738279856da6b32a1ee9dba20849d9cd84b6165166a6abeae1139ed8df8be3b4594d9701309075f2b8d5d4de1f713fb62ae37e1a430a41049c45e552ac9f69c083bd358acac31a2e3cf7d9aa9298fef11b43252730949a39c68272302a61b548b13452e19272c119b5189a5d7b5c3283a37d5d9db5ed0c6818b286deaecc30" + "0aac020a940108d995eeadcc3012460a440a408f20c9fc03909edeb21538b0a568c423f8829e95c0270779ca704f72a45f02416f6071f6faaf421cac3bacc6bb432fc4b5f92bc4391349953c7c98f12253cdd710011a430a4104b7eb7b56059a4f08bf3dd8f1b329e21d486e39822f17db15bad0d7f689f6c8081ae2800b9014fc9ef355a39e10503fddfdfa0b07ccc1946c2275b10e660d5ded12920108e995eeadcc3012440a420a40da669aa014468ffe34d5b962443d8b1e353b1e39f252bbcffa5c6c70adf9f7d2484de944213f345bac869e8c1942657b9c59f6fc12d139171b22789bc76ffb971a430a4104901d3a7f728bde1f871bcf46d44dcf34eead4c532135913583268d35bd93ca0a1571a8cb6546ab333f2d77c3bb9839be7e8f27795ea4d6e979b6670dec20636d12aa020a920108bad3eaadcc3012440a420a4016d83a6e44ee8b9764f18fbb390f2a4049d92ff904ebd75c76a71d58a7f943744f8bed7d3696f9fb41ce450c5ab9f4a7f9a83e3d10f401bbe85e3992c5156d491a430a41047cebe3a23e573672363665d13220d368d37776e10232de9bd382d5af36392956dbd806f8b78bec5cdc111763e4ef4aff7dee65a8a15fee8d338c387320c5b23912920108bad3eaadcc3012440a420a404a751f28001f34a4136529a99e738279856da6b32a1ee9dba20849d9cd84b6165166a6abeae1139ed8df8be3b4594d9701309075f2b8d5d4de1f713fb62ae37e1a430a41049c45e552ac9f69c083bd358acac31a2e3cf7d9aa9298fef11b43252730949a39c68272302a61b548b13452e19272c119b5189a5d7b5c3283a37d5d9db5ed0c6818b286deaecc30", ) val ciphertext = CipherText.newBuilder().apply { aes256GcmHkdfSha256 = aes256GcmHkdfSha256.toBuilder().also { @@ -197,7 +198,7 @@ class MessageTest { val convo = client.conversations.list()[0] convo.send( text = "hello deflate from kotlin again", - SendOptions(compression = EncodedContentCompression.DEFLATE) + SendOptions(compression = EncodedContentCompression.DEFLATE), ) val message = convo.messages().lastOrNull()!! assertEquals("hello deflate from kotlin again", message.content()) @@ -239,7 +240,7 @@ class MessageTest { val convo = ConversationV1( client = client, peerAddress = "0xf4BF19Ed562651837bc11ff975472ABd239D35B5", - sentAt = Date() + sentAt = Date(), ) convo.send(text = "hello from kotlin") val messages = convo.messages() @@ -254,7 +255,7 @@ class MessageTest { val client = Client().create(account = wallet) val convo = client.conversations.newConversation( "0xf4BF19Ed562651837bc11ff975472ABd239D35B5", - InvitationV1ContextBuilder.buildFromConversation("https://example.com/4") + InvitationV1ContextBuilder.buildFromConversation("https://example.com/4"), ) convo.send(content = "hello from kotlin") @@ -279,7 +280,7 @@ class MessageTest { fun testGetsV2ID() { val envelopeMessageData = Numeric.hexStringToByteArray( - "12bf040a470880dedf9dafc0ff9e17123b2f786d74702f302f6d2d32536b644e355161305a6d694649357433524662667749532d4f4c76356a7573716e6465656e544c764e672f70726f746f12f3030af0030a20439174a205643a50af33c7670341338526dbb9c1cf0560687ff8a742e957282d120c090ba2b385b40639867493ce1abd037648c947f72e5c62e8691d7748e78f9a346ff401c97a628ebecf627d722829ff9cfb7d7c3e0b9e26b5801f2b5a39fd58757cc5771427bfefad6243f52cfc84b384fa042873ebeb90948aa80ca34f26ff883d64720c9228ed6bcd1a5c46953a12ae8732fd70260651455674e2e2c23bc8d64ed35562fef4cdfc55d38e72ad9cf2d597e68f48b6909967b0f5d0b4f33c0af3efce55c739fbc93888d20b833df15811823970a356b26622936564d830434d3ecde9a013f7433142e366f1df5589131e440251be54d5d6deef9aaaa9facac26eb54fb7b74eb48c5a2a9a2e2956633b123cc5b91dec03e4dba30683be03bd7510f16103d3f81712dccf2be003f2f77f9e1f162bc47f6c1c38a1068abd3403952bef31d75e8024e7a62d9a8cbd48f1872a0156abb559d01de689b4370a28454658957061c46f47fc5594808d15753876d4b5408b3a3410d0555c016e427dfceae9c05a4a21fd7ce4cfbb11b2a696170443cf310e0083b0a48e357fc2f00c688c0b56821c8a14c2bb44ddfa31d680dfc85efe4811e86c6aa3adfc373ad5731ddab83960774d98d60075b8fd70228da5d748bfb7a5334bd07e1cc4a9fbf3d5de50860d0684bb27786b5b4e00d415" + "12bf040a470880dedf9dafc0ff9e17123b2f786d74702f302f6d2d32536b644e355161305a6d694649357433524662667749532d4f4c76356a7573716e6465656e544c764e672f70726f746f12f3030af0030a20439174a205643a50af33c7670341338526dbb9c1cf0560687ff8a742e957282d120c090ba2b385b40639867493ce1abd037648c947f72e5c62e8691d7748e78f9a346ff401c97a628ebecf627d722829ff9cfb7d7c3e0b9e26b5801f2b5a39fd58757cc5771427bfefad6243f52cfc84b384fa042873ebeb90948aa80ca34f26ff883d64720c9228ed6bcd1a5c46953a12ae8732fd70260651455674e2e2c23bc8d64ed35562fef4cdfc55d38e72ad9cf2d597e68f48b6909967b0f5d0b4f33c0af3efce55c739fbc93888d20b833df15811823970a356b26622936564d830434d3ecde9a013f7433142e366f1df5589131e440251be54d5d6deef9aaaa9facac26eb54fb7b74eb48c5a2a9a2e2956633b123cc5b91dec03e4dba30683be03bd7510f16103d3f81712dccf2be003f2f77f9e1f162bc47f6c1c38a1068abd3403952bef31d75e8024e7a62d9a8cbd48f1872a0156abb559d01de689b4370a28454658957061c46f47fc5594808d15753876d4b5408b3a3410d0555c016e427dfceae9c05a4a21fd7ce4cfbb11b2a696170443cf310e0083b0a48e357fc2f00c688c0b56821c8a14c2bb44ddfa31d680dfc85efe4811e86c6aa3adfc373ad5731ddab83960774d98d60075b8fd70228da5d748bfb7a5334bd07e1cc4a9fbf3d5de50860d0684bb27786b5b4e00d415", ) val envelope = MessageApiOuterClass.Envelope.newBuilder().also { it.contentTopic = "/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto" @@ -288,7 +289,7 @@ class MessageTest { }.build() val ints = arrayOf( 80, 84, 15, 126, 14, 105, 216, 8, 61, 147, 153, 232, 103, 69, 219, 13, - 99, 118, 68, 56, 160, 94, 58, 22, 140, 247, 221, 172, 14, 188, 52, 88 + 99, 118, 68, 56, 160, 94, 58, 22, 140, 247, 221, 172, 14, 188, 52, 88, ) val bytes = ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } @@ -310,7 +311,7 @@ class MessageTest { val client = Client().buildFrom(bundle = keyBundle.v1) val conversationJSON = (""" {"version":"v2","topic":"/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto","keyMaterial":"ATA1L0O2aTxHmskmlGKCudqfGqwA1H+bad3W/GpGOr8=","peerAddress":"0x436D906d1339fC4E951769b1699051f020373D04","createdAt":"2023-01-26T22:58:45.068Z","context":{"conversationId":"pat/messageid","metadata":{}}} """).toByteArray( - UTF_8 + UTF_8, ) val decodedConversation = client.importConversation(conversationJSON) val conversation = ConversationV2( @@ -319,12 +320,12 @@ class MessageTest { context = Context.newBuilder().build(), peerAddress = decodedConversation.peerAddress, client = client, - header = Invitation.SealedInvitationHeaderV1.newBuilder().build() + header = Invitation.SealedInvitationHeaderV1.newBuilder().build(), ) val decodedMessage = conversation.decodeEnvelope(envelope) assertEquals( decodedMessage.id, - "e42a7dd44d0e1214824eab093cb89cfe6f666298d0af2d54fe0c914c8b72eff3" + "e42a7dd44d0e1214824eab093cb89cfe6f666298d0af2d54fe0c914c8b72eff3", ) } @@ -365,12 +366,12 @@ class MessageTest { val aliceSharedSecret = alicePrivateBundle.sharedSecret( peer = bobPublicBundle, myPreKey = alicePublicBundle.preKey, - isRecipient = true + isRecipient = true, ) val bobSharedSecret = bobPrivateBundle.sharedSecret( peer = alicePublicBundle, myPreKey = bobPublicBundle.preKey, - isRecipient = false + isRecipient = false, ) assert(aliceSharedSecret.contentEquals(bobSharedSecret)) } @@ -389,7 +390,7 @@ class MessageTest { val secret = meBundle.sharedSecret( peer = youBundlePublic, myPreKey = meBundle.preKeysList[0].publicKey, - isRecipient = true + isRecipient = true, ) assert(secretData.contentEquals(secret)) } diff --git a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt index 2a13f06ec..0333f1c8c 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt @@ -11,6 +11,7 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema +import org.xmtp.android.library.messages.MessageV2Builder import org.xmtp.android.library.messages.walletAddress @RunWith(AndroidJUnit4::class) @@ -42,7 +43,7 @@ class ReactionTest { "action" to "added", "reference" to "abc123", "schema" to "shortcode", - ) + ), ) it.content = "smile".toByteStringUtf8() }.build() @@ -77,7 +78,7 @@ class ReactionTest { reference = messageToReact.id, action = ReactionAction.Added, content = "U+1F603", - schema = ReactionSchema.Unicode + schema = ReactionSchema.Unicode, ) aliceConversation.send( @@ -94,4 +95,51 @@ class ReactionTest { assertEquals(ReactionSchema.Unicode, content?.schema) } } + + @Test + fun testShouldPushMustBeTrue() { + Client.register(codec = ReactionCodec()) + + val fixtures = fixtures() + val aliceClient = fixtures.aliceClient + val aliceConversation = + aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + + aliceConversation.send(text = "hey alice 2 bob") + + val messageToReact = aliceConversation.messages()[0] + + val attachment = Reaction( + reference = messageToReact.id, + action = ReactionAction.Added, + content = "U+1F603", + schema = ReactionSchema.Unicode, + ) + + aliceConversation.send( + content = attachment, + options = SendOptions(contentType = ContentTypeReaction), + ) + val messages = aliceConversation.messages() + assertEquals(messages.size, 2) + + val message = MessageV2Builder.buildEncode( + client = aliceClient, + encodedContent = messages[0].encodedContent, + topic = aliceConversation.topic, + keyMaterial = aliceConversation.keyMaterial!!, + codec = ReactionCodec(), + ) + + if (messages.size == 2) { + val content: Reaction? = messages.first().content() + assertEquals("U+1F603", content?.content) + assertEquals(messageToReact.id, content?.reference) + assertEquals(ReactionAction.Added, content?.action) + assertEquals(ReactionSchema.Unicode, content?.schema) + } + + assertEquals(true, message.shouldPush) + assertEquals(true, message.senderHmac?.isNotEmpty()) + } } diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt index 6d20253c6..a0c46fbfe 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt @@ -84,7 +84,7 @@ data class ConversationV2( val result = runBlocking { client.apiClient.envelopes( topic = topic, - pagination = pagination + pagination = pagination, ) } @@ -133,7 +133,7 @@ data class ConversationV2( topic, message.v2, keyMaterial, - client + client, ) } @@ -155,7 +155,7 @@ data class ConversationV2( topic = topic, message.v2, keyMaterial = keyMaterial, - client = client + client = client, ) } @@ -202,12 +202,13 @@ data class ConversationV2( client = client, encodedContent = encodedContent, topic = topic, - keyMaterial = keyMaterial + keyMaterial = keyMaterial, + codec = codec, ) val envelope = EnvelopeBuilder.buildFromString( topic = topic, timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray() + message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), ) return envelope.toByteArray() } @@ -238,12 +239,17 @@ data class ConversationV2( return prepareMessage(encoded, options = options) } - fun prepareMessage(encodedContent: EncodedContent, options: SendOptions?): PreparedMessage { + fun prepareMessage( + encodedContent: EncodedContent, + options: SendOptions?, + ): PreparedMessage { + val codec = Client.codecRegistry.find(options?.contentType) val message = MessageV2Builder.buildEncode( client = client, encodedContent = encodedContent, topic = topic, - keyMaterial = keyMaterial + keyMaterial = keyMaterial, + codec = codec, ) val newTopic = if (options?.ephemeral == true) ephemeralTopic else topic @@ -251,7 +257,7 @@ data class ConversationV2( val envelope = EnvelopeBuilder.buildFromString( topic = newTopic, timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray() + message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), ) return PreparedMessage(listOf(envelope)) } diff --git a/library/src/main/java/org/xmtp/android/library/Conversations.kt b/library/src/main/java/org/xmtp/android/library/Conversations.kt index cec8da0de..535efe0fd 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -1,6 +1,8 @@ package org.xmtp.android.library import android.util.Log +import com.google.protobuf.kotlin.toByteString +import com.google.protobuf.kotlin.toByteStringUtf8 import io.grpc.StatusException import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.awaitClose @@ -33,6 +35,9 @@ import org.xmtp.android.library.messages.senderAddress import org.xmtp.android.library.messages.sentAt import org.xmtp.android.library.messages.toSignedPublicKeyBundle import org.xmtp.android.library.messages.walletAddress +import org.xmtp.proto.keystore.api.v1.Keystore +import org.xmtp.proto.keystore.api.v1.Keystore.GetConversationHmacKeysResponse.HmacKeyData +import org.xmtp.proto.keystore.api.v1.Keystore.GetConversationHmacKeysResponse.HmacKeys import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData import org.xmtp.proto.message.contents.Contact import org.xmtp.proto.message.contents.Invitation @@ -298,6 +303,42 @@ data class Conversations( return conversation } + fun getHmacKeys( + request: Keystore.GetConversationHmacKeysRequest? = null, + ): Keystore.GetConversationHmacKeysResponse { + val thirtyDayPeriodsSinceEpoch = (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() + val hmacKeysResponse = Keystore.GetConversationHmacKeysResponse.newBuilder() + + var topics = conversationsByTopic + + if (!request?.topicsList.isNullOrEmpty()) { + topics = topics.filter { + request!!.topicsList.contains(it.key) + }.toMutableMap() + } + + topics.forEach { + val conversation = it.value + val hmacKeys = HmacKeys.newBuilder() + if (conversation.keyMaterial != null) { + (thirtyDayPeriodsSinceEpoch - 1..thirtyDayPeriodsSinceEpoch + 1).forEach { value -> + val info = "$value-${client.address}" + val hmacKey = + Crypto.calculateMac( + conversation.keyMaterial!!, + info.toByteStringUtf8().toByteArray() + ) + val hmacKeyData = HmacKeyData.newBuilder() + hmacKeyData.hmacKey = hmacKey.toByteString() + hmacKeyData.thirtyDayPeriodsSinceEpoch = value + hmacKeys.addValues(hmacKeyData) + } + hmacKeysResponse.putHmacKeys(conversation.topic, hmacKeys.build()) + } + } + return hmacKeysResponse.build() + } + private fun listIntroductionPeers(pagination: Pagination? = null): Map { val envelopes = runBlocking { diff --git a/library/src/main/java/org/xmtp/android/library/Crypto.kt b/library/src/main/java/org/xmtp/android/library/Crypto.kt index b91af0fa9..2160baa91 100644 --- a/library/src/main/java/org/xmtp/android/library/Crypto.kt +++ b/library/src/main/java/org/xmtp/android/library/Crypto.kt @@ -3,9 +3,6 @@ package org.xmtp.android.library import android.util.Log import com.google.crypto.tink.subtle.Hkdf import com.google.protobuf.kotlin.toByteString -import org.bouncycastle.crypto.digests.SHA256Digest -import org.bouncycastle.crypto.generators.HKDFBytesGenerator -import org.bouncycastle.crypto.params.HKDFParameters import org.xmtp.proto.message.contents.CiphertextOuterClass import java.security.GeneralSecurityException import java.security.SecureRandom @@ -77,26 +74,40 @@ class Crypto { null } } - } - fun calculateMac(secret: ByteArray, message: ByteArray): ByteArray { - val sha256HMAC: Mac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(secret, "HmacSHA256") - sha256HMAC.init(secretKey) - return sha256HMAC.doFinal(message) - } + fun calculateMac(secret: ByteArray, message: ByteArray): ByteArray { + val sha256HMAC: Mac = Mac.getInstance("HmacSHA256") + val secretKey = SecretKeySpec(secret, "HmacSHA256") + sha256HMAC.init(secretKey) + return sha256HMAC.doFinal(message) + } + + fun deriveKey( + secret: ByteArray, + salt: ByteArray, + info: ByteArray, + ): ByteArray { + val keySpec = SecretKeySpec(secret, "HmacSHA256") + val hmac = Mac.getInstance("HmacSHA256") + hmac.init(keySpec) + val derivedKey = hmac.doFinal(salt + info) - fun deriveKey( - secret: ByteArray, - salt: ByteArray, - info: ByteArray, - ): ByteArray { - val derivationParameters = HKDFParameters(secret, salt, info) - val digest = SHA256Digest() - val hkdfGenerator = HKDFBytesGenerator(digest) - hkdfGenerator.init(derivationParameters) - val hkdf = ByteArray(32) - hkdfGenerator.generateBytes(hkdf, 0, hkdf.size) - return hkdf + return derivedKey.copyOfRange(0, 32) + } + + fun verifyHmacSignature( + key: ByteArray, + signature: ByteArray, + message: ByteArray + ): Boolean { + return try { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(key, "HmacSHA256")) + val computedSignature = mac.doFinal(message) + computedSignature.contentEquals(signature) + } catch (e: Exception) { + false + } + } } } diff --git a/library/src/main/java/org/xmtp/android/library/codecs/AttachmentCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/AttachmentCodec.kt index 21c9e872c..80d39d2b8 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/AttachmentCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/AttachmentCodec.kt @@ -36,4 +36,6 @@ data class AttachmentCodec(override var contentType: ContentTypeId = ContentType override fun fallback(content: Attachment): String? { return "Can’t display \"${content.filename}”. This app doesn’t support attachments." } + + override fun shouldPush(content: Attachment): Boolean = true } diff --git a/library/src/main/java/org/xmtp/android/library/codecs/Composite.kt b/library/src/main/java/org/xmtp/android/library/codecs/Composite.kt index 4fa7c8c9f..da233e5bc 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/Composite.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/Composite.kt @@ -52,6 +52,8 @@ class CompositeCodec : ContentCodec { return null } + override fun shouldPush(content: DecodedComposite): Boolean = false + private fun toComposite(decodedComposite: DecodedComposite): Composite { return Composite.newBuilder().also { val content = decodedComposite.encodedContent diff --git a/library/src/main/java/org/xmtp/android/library/codecs/ContentCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/ContentCodec.kt index 1282e3655..22e4190d7 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/ContentCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/ContentCodec.kt @@ -67,6 +67,7 @@ interface ContentCodec { fun encode(content: T): EncodedContent fun decode(content: EncodedContent): T fun fallback(content: T): String? + fun shouldPush(content: T): Boolean } val id: String diff --git a/library/src/main/java/org/xmtp/android/library/codecs/GroupMembershipChangeCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/GroupMembershipChangeCodec.kt index 9d7e855fd..eb153c146 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/GroupMembershipChangeCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/GroupMembershipChangeCodec.kt @@ -11,7 +11,7 @@ val ContentTypeGroupMembershipChange = ContentTypeIdBuilder.builderFromAuthority "xmtp.org", "group_membership_change", versionMajor = 1, - versionMinor = 0 + versionMinor = 0, ) data class GroupMembershipChangeCodec(override var contentType: ContentTypeId = ContentTypeGroupMembershipChange) : @@ -31,4 +31,6 @@ data class GroupMembershipChangeCodec(override var contentType: ContentTypeId = override fun fallback(content: GroupMembershipChanges): String? { return null } + + override fun shouldPush(content: GroupMembershipChanges): Boolean = false } diff --git a/library/src/main/java/org/xmtp/android/library/codecs/ReactionCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/ReactionCodec.kt index f15f366d4..cb0093a63 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/ReactionCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/ReactionCodec.kt @@ -14,7 +14,7 @@ val ContentTypeReaction = ContentTypeIdBuilder.builderFromAuthorityId( "xmtp.org", "reaction", versionMajor = 1, - versionMinor = 0 + versionMinor = 0, ) data class Reaction( @@ -95,6 +95,11 @@ data class ReactionCodec(override var contentType: ContentTypeId = ContentTypeRe else -> null } } + + override fun shouldPush(content: Reaction): Boolean = when (content.action) { + ReactionAction.Added -> true + else -> false + } } private class ReactionSerializer : JsonSerializer { diff --git a/library/src/main/java/org/xmtp/android/library/codecs/ReadReceiptCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/ReadReceiptCodec.kt index 6e51e77ae..ec5c76cd2 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/ReadReceiptCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/ReadReceiptCodec.kt @@ -6,7 +6,7 @@ val ContentTypeReadReceipt = ContentTypeIdBuilder.builderFromAuthorityId( "xmtp.org", "readReceipt", versionMajor = 1, - versionMinor = 0 + versionMinor = 0, ) object ReadReceipt @@ -28,4 +28,6 @@ data class ReadReceiptCodec(override var contentType: ContentTypeId = ContentTyp override fun fallback(content: ReadReceipt): String? { return null } + + override fun shouldPush(content: ReadReceipt): Boolean = false } diff --git a/library/src/main/java/org/xmtp/android/library/codecs/RemoteAttachmentCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/RemoteAttachmentCodec.kt index 2fdebe0a2..5e781d1b2 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/RemoteAttachmentCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/RemoteAttachmentCodec.kt @@ -80,8 +80,10 @@ data class RemoteAttachment( fun encodeEncrypted(content: T, codec: ContentCodec): EncryptedEncodedContent { val secret = SecureRandom().generateSeed(32) val encodedContent = codec.encode(content).toByteArray() - val ciphertext = Crypto.encrypt(secret, encodedContent) ?: throw XMTPException("ciphertext not created") - val contentDigest = Hash.sha256(ciphertext.aes256GcmHkdfSha256.payload.toByteArray()).toHex() + val ciphertext = Crypto.encrypt(secret, encodedContent) + ?: throw XMTPException("ciphertext not created") + val contentDigest = + Hash.sha256(ciphertext.aes256GcmHkdfSha256.payload.toByteArray()).toHex() return EncryptedEncodedContent( contentDigest = contentDigest, secret = secret.toByteString(), @@ -114,7 +116,7 @@ val ContentTypeRemoteAttachment = ContentTypeIdBuilder.builderFromAuthorityId( "xmtp.org", "remoteStaticAttachment", versionMajor = 1, - versionMinor = 0 + versionMinor = 0, ) interface Fetcher { @@ -127,7 +129,8 @@ class HTTPFetcher : Fetcher { } } -data class RemoteAttachmentCodec(override var contentType: ContentTypeId = ContentTypeRemoteAttachment) : ContentCodec { +data class RemoteAttachmentCodec(override var contentType: ContentTypeId = ContentTypeRemoteAttachment) : + ContentCodec { override fun encode(content: RemoteAttachment): EncodedContent { return EncodedContent.newBuilder().also { it.type = ContentTypeRemoteAttachment @@ -140,19 +143,21 @@ data class RemoteAttachmentCodec(override var contentType: ContentTypeId = Conte "scheme" to content.scheme, "contentLength" to content.contentLength.toString(), "filename" to content.filename, - ) + ), ) it.content = content.url.toString().toByteStringUtf8() }.build() } override fun decode(content: EncodedContent): RemoteAttachment { - val contentDigest = content.parametersMap["contentDigest"] ?: throw XMTPException("missing content digest") + val contentDigest = + content.parametersMap["contentDigest"] ?: throw XMTPException("missing content digest") val secret = content.parametersMap["secret"] ?: throw XMTPException("missing secret") val salt = content.parametersMap["salt"] ?: throw XMTPException("missing salt") val nonce = content.parametersMap["nonce"] ?: throw XMTPException("missing nonce") val scheme = content.parametersMap["scheme"] ?: throw XMTPException("missing scheme") - val contentLength = content.parametersMap["contentLength"] ?: throw XMTPException("missing contentLength") + val contentLength = + content.parametersMap["contentLength"] ?: throw XMTPException("missing contentLength") val filename = content.parametersMap["filename"] ?: throw XMTPException("missing filename") val encodedContent = content.content ?: throw XMTPException("missing content") @@ -171,4 +176,6 @@ data class RemoteAttachmentCodec(override var contentType: ContentTypeId = Conte override fun fallback(content: RemoteAttachment): String? { return "Can’t display \"${content.filename}”. This app doesn’t support attachments." } + + override fun shouldPush(content: RemoteAttachment): Boolean = true } diff --git a/library/src/main/java/org/xmtp/android/library/codecs/ReplyCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/ReplyCodec.kt index f61c20400..e60780123 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/ReplyCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/ReplyCodec.kt @@ -7,7 +7,7 @@ val ContentTypeReply = ContentTypeIdBuilder.builderFromAuthorityId( "xmtp.org", "reply", versionMajor = 1, - versionMinor = 0 + versionMinor = 0, ) data class Reply( @@ -41,7 +41,7 @@ data class ReplyCodec(override var contentType: ContentTypeId = ContentTypeReply return Reply( reference = reference, content = replyContent, - contentType = replyCodec.contentType + contentType = replyCodec.contentType, ) } @@ -49,6 +49,8 @@ data class ReplyCodec(override var contentType: ContentTypeId = ContentTypeReply return "Replied with “${content.content}” to an earlier message" } + override fun shouldPush(content: Reply): Boolean = true + private fun , T> encodeReply( codec: Codec, content: Any, diff --git a/library/src/main/java/org/xmtp/android/library/codecs/TextCodec.kt b/library/src/main/java/org/xmtp/android/library/codecs/TextCodec.kt index 378d6f060..66a7ea9cc 100644 --- a/library/src/main/java/org/xmtp/android/library/codecs/TextCodec.kt +++ b/library/src/main/java/org/xmtp/android/library/codecs/TextCodec.kt @@ -36,4 +36,6 @@ data class TextCodec(override var contentType: ContentTypeId = ContentTypeText) override fun fallback(content: String): String? { return null } + + override fun shouldPush(content: String): Boolean = true } diff --git a/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt b/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt index 347847ed7..f0cd5e57c 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt @@ -52,7 +52,7 @@ fun InvitationV1.createRandom(context: Context? = null): InvitationV1 { return InvitationV1Builder.buildFromTopic( topic = topic, context = inviteContext, - aes256GcmHkdfSha256 = aes256GcmHkdfSha256 + aes256GcmHkdfSha256 = aes256GcmHkdfSha256, ) } @@ -68,7 +68,7 @@ fun InvitationV1.createDeterministic( val secret = sender.sharedSecret( peer = recipient, myPreKey = sender.preKeysList[0].publicKey, - isRecipient = myAddress < theirAddress + isRecipient = myAddress < theirAddress, ) val addresses = arrayOf(myAddress, theirAddress) @@ -80,12 +80,12 @@ fun InvitationV1.createDeterministic( addresses.joinToString(separator = ",") } - val topicId = Crypto().calculateMac(secret = secret, message = msg.toByteArray()).toHex() + val topicId = Crypto.calculateMac(secret = secret, message = msg.toByteArray()).toHex() val topic = Topic.directMessageV2(topicId) - val keyMaterial = Crypto().deriveKey( + val keyMaterial = Crypto.deriveKey( secret = secret, salt = "__XMTP__INVITATION__SALT__XMTP__".toByteArray(), - info = listOf("0").plus(addresses).joinToString(separator = "|").toByteArray() + info = listOf("0").plus(addresses).joinToString(separator = "|").toByteArray(), ) val aes256GcmHkdfSha256 = Invitation.InvitationV1.Aes256gcmHkdfsha256.newBuilder().apply { this.keyMaterial = keyMaterial.toByteString() @@ -94,7 +94,7 @@ fun InvitationV1.createDeterministic( return InvitationV1Builder.buildFromTopic( topic = topic, context = inviteContext, - aes256GcmHkdfSha256 = aes256GcmHkdfSha256 + aes256GcmHkdfSha256 = aes256GcmHkdfSha256, ) } diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt index 37b819ed7..e6f9302e7 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt @@ -1,6 +1,7 @@ package org.xmtp.android.library.messages import com.google.protobuf.kotlin.toByteString +import com.google.protobuf.kotlin.toByteStringUtf8 import org.web3j.crypto.ECDSASignature import org.web3j.crypto.Hash import org.web3j.crypto.Sign @@ -10,19 +11,30 @@ import org.xmtp.android.library.Crypto import org.xmtp.android.library.DecodedMessage import org.xmtp.android.library.KeyUtil import org.xmtp.android.library.XMTPException +import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.EncodedContent import java.math.BigInteger import java.util.Date typealias MessageV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV2 -class MessageV2Builder { +class MessageV2Builder(val senderHmac: ByteArray? = null, val shouldPush: Boolean = false) { + lateinit var messageV2: MessageV2 + companion object { - fun buildFromCipherText(headerBytes: ByteArray, ciphertext: CipherText?): MessageV2 { - return MessageV2.newBuilder().also { + fun buildFromCipherText( + headerBytes: ByteArray, + ciphertext: CipherText?, + senderHmac: ByteArray?, + shouldPush: Boolean, + ): MessageV2Builder { + val messageBuilder = MessageV2Builder(senderHmac = senderHmac, shouldPush = shouldPush) + messageBuilder.messageV2 = MessageV2.newBuilder().also { it.headerBytes = headerBytes.toByteString() it.ciphertext = ciphertext + it.shouldPush = shouldPush }.build() + return messageBuilder } fun buildDecode( @@ -41,7 +53,7 @@ class MessageV2Builder { topic = decryptedMessage.topic, encodedContent = decryptedMessage.encodedContent, senderAddress = decryptedMessage.senderAddress, - sent = decryptedMessage.sentAt + sent = decryptedMessage.sentAt, ) } catch (e: Exception) { throw XMTPException("Error decoding message", e) @@ -69,7 +81,7 @@ class MessageV2Builder { if (!senderPreKey.signature.verify( senderIdentityKey, - signed.sender.preKey.keyBytes.toByteArray() + signed.sender.preKey.keyBytes.toByteArray(), ) ) { throw XMTPException("pre-key not signed by identity key") @@ -109,16 +121,25 @@ class MessageV2Builder { encodedContent = encodedMessage, senderAddress = signed.sender.walletAddress, sentAt = Date(header.createdNs / 1_000_000), - topic = topic + topic = topic, ) } - fun buildEncode( + private fun , T> shouldPush(codec: Codec, content: T?): Boolean { + if (content != null) { + return codec.shouldPush(content = content) + } else { + throw XMTPException("Codec invalid content") + } + } + + fun , T> buildEncode( client: Client, encodedContent: EncodedContent, topic: String, keyMaterial: ByteArray, - ): MessageV2 { + codec: Codec, + ): MessageV2Builder { val payload = encodedContent.toByteArray() val date = Date() val header = MessageHeaderV2Builder.buildFromTopic(topic, date) @@ -130,7 +151,23 @@ class MessageV2Builder { val signedContent = SignedContentBuilder.builderFromPayload(payload, bundle, signature) val signedBytes = signedContent.toByteArray() val ciphertext = Crypto.encrypt(keyMaterial, signedBytes, additionalData = headerBytes) - return buildFromCipherText(headerBytes, ciphertext) + + val thirtyDayPeriodsSinceEpoch = + (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() + val info = "$thirtyDayPeriodsSinceEpoch-${client.address}" + val infoEncoded = info.toByteStringUtf8().toByteArray() + val senderHmacGenerated = + Crypto.calculateMac( + Crypto.deriveKey(keyMaterial, ByteArray(0), infoEncoded), + headerBytes + ) + + return buildFromCipherText( + headerBytes, + ciphertext, + senderHmacGenerated, + shouldPush(codec = codec, content = codec.decode(encodedContent)), + ) } } } diff --git a/library/src/test/java/org/xmtp/android/library/RemoteAttachmentTest.kt b/library/src/test/java/org/xmtp/android/library/RemoteAttachmentTest.kt index b459834b2..44093f67c 100644 --- a/library/src/test/java/org/xmtp/android/library/RemoteAttachmentTest.kt +++ b/library/src/test/java/org/xmtp/android/library/RemoteAttachmentTest.kt @@ -60,7 +60,7 @@ class RemoteAttachmentTest { val remoteAttachment = RemoteAttachment.from( url = URL("https://abcdefg"), - encryptedEncodedContent = encodedEncryptedContent + encryptedEncodedContent = encodedEncryptedContent, ) remoteAttachment.contentLength = attachment.data.size() @@ -113,7 +113,7 @@ class RemoteAttachmentTest { Assert.assertThrows(XMTPException::class.java) { RemoteAttachment.from( url = URL("http://abcdefg"), - encryptedEncodedContent = encodedEncryptedContent + encryptedEncodedContent = encodedEncryptedContent, ) } } @@ -139,7 +139,7 @@ class RemoteAttachmentTest { val remoteAttachment = RemoteAttachment.from( url = URL("https://abcdefg"), - encryptedEncodedContent = encodedEncryptedContent + encryptedEncodedContent = encodedEncryptedContent, ) remoteAttachment.contentLength = attachment.data.size()