Skip to content

Commit 95dda24

Browse files
committed
Adding new approach for codecs and crypto
1 parent 9f12fb1 commit 95dda24

File tree

16 files changed

+96
-42
lines changed

16 files changed

+96
-42
lines changed

library/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ dependencies {
8686
implementation 'org.web3j:crypto:5.0.0'
8787
implementation "net.java.dev.jna:jna:5.13.0@aar"
8888
api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
89-
api 'org.xmtp:proto-kotlin:3.39.0'
89+
api 'org.xmtp:proto-kotlin:3.40.0'
9090

9191
testImplementation 'junit:junit:4.13.2'
9292
androidTestImplementation 'app.cash.turbine:turbine:0.12.1'

library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ data class NumberCodec(
3737
override fun decode(content: EncodedContent): Double =
3838
content.content.toStringUtf8().filter { it.isDigit() || it == '.' }.toDouble()
3939

40-
override fun shouldPush(): Boolean = false
40+
override fun shouldPush(content: Double): Boolean = false
4141

4242
override fun fallback(content: Double): String? {
4343
return "Error: This app does not support numbers."

library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,12 @@ class ConversationTest {
229229
additionalData = headerBytes,
230230
)
231231
val tamperedMessage =
232-
MessageV2Builder.buildFromCipherText(headerBytes = headerBytes, ciphertext = ciphertext)
232+
MessageV2Builder.buildFromCipherText(
233+
headerBytes = headerBytes,
234+
ciphertext = ciphertext,
235+
senderHmac = null,
236+
shouldPush = true,
237+
)
233238
val tamperedEnvelope = EnvelopeBuilder.buildFromString(
234239
topic = aliceConversation.topic,
235240
timestamp = Date(),
@@ -585,6 +590,7 @@ class ConversationTest {
585590
encodedContent,
586591
topic = conversation.topic,
587592
keyMaterial = conversation.keyMaterial!!,
593+
shouldPush = true,
588594
),
589595
).toByteArray(),
590596
),

library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ class MessageTest {
9191
client = client,
9292
encodedContent,
9393
topic = invitationv1.topic,
94-
keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray()
94+
keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray(),
95+
shouldPush = true,
9596
)
9697
val decoded = MessageV2Builder.buildDecode(
9798
id = "",

library/src/main/java/org/xmtp/android/library/Conversation.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@ sealed class Conversation {
140140
}
141141

142142
is V2 -> {
143-
conversationV2.prepareMessage(encodedContent = encodedContent, options = options)
143+
conversationV2.prepareMessage(
144+
encodedContent = encodedContent,
145+
options = options,
146+
false,
147+
)
144148
}
145149
}
146150
}

library/src/main/java/org/xmtp/android/library/ConversationV2.kt

+30-10
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ data class ConversationV2(
8484
val result = runBlocking {
8585
client.apiClient.envelopes(
8686
topic = topic,
87-
pagination = pagination
87+
pagination = pagination,
8888
)
8989
}
9090

@@ -133,7 +133,7 @@ data class ConversationV2(
133133
topic,
134134
message.v2,
135135
keyMaterial,
136-
client
136+
client,
137137
)
138138
}
139139

@@ -155,7 +155,7 @@ data class ConversationV2(
155155
topic = topic,
156156
message.v2,
157157
keyMaterial = keyMaterial,
158-
client = client
158+
client = client,
159159
)
160160
}
161161

@@ -184,7 +184,12 @@ data class ConversationV2(
184184
}
185185

186186
fun send(encodedContent: EncodedContent, options: SendOptions?): String {
187-
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
187+
val codec = Client.codecRegistry.find(options?.contentType)
188+
val preparedMessage = prepareMessage(
189+
encodedContent = encodedContent,
190+
options = options,
191+
shouldPush = shouldPush(codec, encodedContent.content),
192+
)
188193
return send(preparedMessage)
189194
}
190195

@@ -202,16 +207,26 @@ data class ConversationV2(
202207
client = client,
203208
encodedContent = encodedContent,
204209
topic = topic,
205-
keyMaterial = keyMaterial
210+
keyMaterial = keyMaterial,
211+
shouldPush = shouldPush(codec, content),
206212
)
207213
val envelope = EnvelopeBuilder.buildFromString(
208214
topic = topic,
209215
timestamp = Date(),
210-
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
216+
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray(),
211217
)
212218
return envelope.toByteArray()
213219
}
214220

221+
fun <Codec : ContentCodec<T>, T> shouldPush(codec: Codec, content: Any?): Boolean {
222+
val contentType = content as? T
223+
if (contentType != null) {
224+
return codec.shouldPush(content = content)
225+
} else {
226+
throw XMTPException("Codec invalid content")
227+
}
228+
}
229+
215230
fun <T> prepareMessage(content: T, options: SendOptions?): PreparedMessage {
216231
val codec = Client.codecRegistry.find(options?.contentType)
217232

@@ -235,23 +250,28 @@ data class ConversationV2(
235250
if (compression != null) {
236251
encoded = encoded.compress(compression)
237252
}
238-
return prepareMessage(encoded, options = options)
253+
return prepareMessage(encoded, options = options, shouldPush = shouldPush(codec, content))
239254
}
240255

241-
fun prepareMessage(encodedContent: EncodedContent, options: SendOptions?): PreparedMessage {
256+
fun prepareMessage(
257+
encodedContent: EncodedContent,
258+
options: SendOptions?,
259+
shouldPush: Boolean,
260+
): PreparedMessage {
242261
val message = MessageV2Builder.buildEncode(
243262
client = client,
244263
encodedContent = encodedContent,
245264
topic = topic,
246-
keyMaterial = keyMaterial
265+
keyMaterial = keyMaterial,
266+
shouldPush = shouldPush,
247267
)
248268

249269
val newTopic = if (options?.ephemeral == true) ephemeralTopic else topic
250270

251271
val envelope = EnvelopeBuilder.buildFromString(
252272
topic = newTopic,
253273
timestamp = Date(),
254-
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
274+
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray(),
255275
)
256276
return PreparedMessage(listOf(envelope))
257277
}

library/src/main/java/org/xmtp/android/library/codecs/AttachmentCodec.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@ data class AttachmentCodec(override var contentType: ContentTypeId = ContentType
3737
return "Can’t display \"${content.filename}”. This app doesn’t support attachments."
3838
}
3939

40-
override fun shouldPush(): Boolean = true
40+
override fun shouldPush(content: Attachment): Boolean = true
4141
}

library/src/main/java/org/xmtp/android/library/codecs/Composite.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class CompositeCodec : ContentCodec<DecodedComposite> {
5252
return null
5353
}
5454

55-
override fun shouldPush(): Boolean = false
55+
override fun shouldPush(content: DecodedComposite): Boolean = false
5656

5757
private fun toComposite(decodedComposite: DecodedComposite): Composite {
5858
return Composite.newBuilder().also {

library/src/main/java/org/xmtp/android/library/codecs/ContentCodec.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ interface ContentCodec<T> {
6767
fun encode(content: T): EncodedContent
6868
fun decode(content: EncodedContent): T
6969
fun fallback(content: T): String?
70-
fun shouldPush(): Boolean
70+
fun shouldPush(content: T): Boolean
7171
}
7272

7373
val id: String

library/src/main/java/org/xmtp/android/library/codecs/ReactionCodec.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ val ContentTypeReaction = ContentTypeIdBuilder.builderFromAuthorityId(
1414
"xmtp.org",
1515
"reaction",
1616
versionMajor = 1,
17-
versionMinor = 0
17+
versionMinor = 0,
1818
)
1919

2020
data class Reaction(
@@ -96,7 +96,11 @@ data class ReactionCodec(override var contentType: ContentTypeId = ContentTypeRe
9696
}
9797
}
9898

99-
override fun shouldPush(): Boolean = true
99+
override fun shouldPush(content: Reaction): Boolean = when (content.action) {
100+
ReactionAction.Added -> true
101+
ReactionAction.Removed -> false
102+
ReactionAction.Unknown -> false
103+
}
100104
}
101105

102106
private class ReactionSerializer : JsonSerializer<Reaction> {

library/src/main/java/org/xmtp/android/library/codecs/ReadReceiptCodec.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ val ContentTypeReadReceipt = ContentTypeIdBuilder.builderFromAuthorityId(
66
"xmtp.org",
77
"readReceipt",
88
versionMajor = 1,
9-
versionMinor = 0
9+
versionMinor = 0,
1010
)
1111

1212
object ReadReceipt
@@ -29,5 +29,5 @@ data class ReadReceiptCodec(override var contentType: ContentTypeId = ContentTyp
2929
return null
3030
}
3131

32-
override fun shouldPush(): Boolean = false
32+
override fun shouldPush(content: ReadReceipt): Boolean = false
3333
}

library/src/main/java/org/xmtp/android/library/codecs/RemoteAttachmentCodec.kt

+13-8
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ data class RemoteAttachment(
8080
fun <T> encodeEncrypted(content: T, codec: ContentCodec<T>): EncryptedEncodedContent {
8181
val secret = SecureRandom().generateSeed(32)
8282
val encodedContent = codec.encode(content).toByteArray()
83-
val ciphertext = Crypto.encrypt(secret, encodedContent) ?: throw XMTPException("ciphertext not created")
84-
val contentDigest = Hash.sha256(ciphertext.aes256GcmHkdfSha256.payload.toByteArray()).toHex()
83+
val ciphertext = Crypto.encrypt(secret, encodedContent)
84+
?: throw XMTPException("ciphertext not created")
85+
val contentDigest =
86+
Hash.sha256(ciphertext.aes256GcmHkdfSha256.payload.toByteArray()).toHex()
8587
return EncryptedEncodedContent(
8688
contentDigest = contentDigest,
8789
secret = secret.toByteString(),
@@ -114,7 +116,7 @@ val ContentTypeRemoteAttachment = ContentTypeIdBuilder.builderFromAuthorityId(
114116
"xmtp.org",
115117
"remoteStaticAttachment",
116118
versionMajor = 1,
117-
versionMinor = 0
119+
versionMinor = 0,
118120
)
119121

120122
interface Fetcher {
@@ -127,7 +129,8 @@ class HTTPFetcher : Fetcher {
127129
}
128130
}
129131

130-
data class RemoteAttachmentCodec(override var contentType: ContentTypeId = ContentTypeRemoteAttachment) : ContentCodec<RemoteAttachment> {
132+
data class RemoteAttachmentCodec(override var contentType: ContentTypeId = ContentTypeRemoteAttachment) :
133+
ContentCodec<RemoteAttachment> {
131134
override fun encode(content: RemoteAttachment): EncodedContent {
132135
return EncodedContent.newBuilder().also {
133136
it.type = ContentTypeRemoteAttachment
@@ -140,19 +143,21 @@ data class RemoteAttachmentCodec(override var contentType: ContentTypeId = Conte
140143
"scheme" to content.scheme,
141144
"contentLength" to content.contentLength.toString(),
142145
"filename" to content.filename,
143-
)
146+
),
144147
)
145148
it.content = content.url.toString().toByteStringUtf8()
146149
}.build()
147150
}
148151

149152
override fun decode(content: EncodedContent): RemoteAttachment {
150-
val contentDigest = content.parametersMap["contentDigest"] ?: throw XMTPException("missing content digest")
153+
val contentDigest =
154+
content.parametersMap["contentDigest"] ?: throw XMTPException("missing content digest")
151155
val secret = content.parametersMap["secret"] ?: throw XMTPException("missing secret")
152156
val salt = content.parametersMap["salt"] ?: throw XMTPException("missing salt")
153157
val nonce = content.parametersMap["nonce"] ?: throw XMTPException("missing nonce")
154158
val scheme = content.parametersMap["scheme"] ?: throw XMTPException("missing scheme")
155-
val contentLength = content.parametersMap["contentLength"] ?: throw XMTPException("missing contentLength")
159+
val contentLength =
160+
content.parametersMap["contentLength"] ?: throw XMTPException("missing contentLength")
156161
val filename = content.parametersMap["filename"] ?: throw XMTPException("missing filename")
157162
val encodedContent = content.content ?: throw XMTPException("missing content")
158163

@@ -172,5 +177,5 @@ data class RemoteAttachmentCodec(override var contentType: ContentTypeId = Conte
172177
return "Can’t display \"${content.filename}”. This app doesn’t support attachments."
173178
}
174179

175-
override fun shouldPush(): Boolean = true
180+
override fun shouldPush(content: RemoteAttachment): Boolean = true
176181
}

library/src/main/java/org/xmtp/android/library/codecs/ReplyCodec.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ val ContentTypeReply = ContentTypeIdBuilder.builderFromAuthorityId(
77
"xmtp.org",
88
"reply",
99
versionMajor = 1,
10-
versionMinor = 0
10+
versionMinor = 0,
1111
)
1212

1313
data class Reply(
@@ -41,15 +41,15 @@ data class ReplyCodec(override var contentType: ContentTypeId = ContentTypeReply
4141
return Reply(
4242
reference = reference,
4343
content = replyContent,
44-
contentType = replyCodec.contentType
44+
contentType = replyCodec.contentType,
4545
)
4646
}
4747

4848
override fun fallback(content: Reply): String? {
4949
return "Replied with “${content.content}” to an earlier message"
5050
}
5151

52-
override fun shouldPush(): Boolean = true
52+
override fun shouldPush(content: Reply): Boolean = true
5353

5454
private fun <Codec : ContentCodec<T>, T> encodeReply(
5555
codec: Codec,

library/src/main/java/org/xmtp/android/library/codecs/TextCodec.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@ data class TextCodec(override var contentType: ContentTypeId = ContentTypeText)
3737
return null
3838
}
3939

40-
override fun shouldPush(): Boolean = true
40+
override fun shouldPush(content: String): Boolean = true
4141
}

library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.xmtp.android.library.messages
22

33
import com.google.protobuf.kotlin.toByteString
4+
import com.google.protobuf.kotlin.toByteStringUtf8
45
import org.web3j.crypto.ECDSASignature
56
import org.web3j.crypto.Hash
67
import org.web3j.crypto.Sign
@@ -18,7 +19,12 @@ typealias MessageV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV
1819

1920
class MessageV2Builder {
2021
companion object {
21-
fun buildFromCipherText(headerBytes: ByteArray, ciphertext: CipherText?): MessageV2 {
22+
fun buildFromCipherText(
23+
headerBytes: ByteArray,
24+
ciphertext: CipherText?,
25+
senderHmac: ByteArray?,
26+
shouldPush: Boolean,
27+
): MessageV2 {
2228
return MessageV2.newBuilder().also {
2329
it.headerBytes = headerBytes.toByteString()
2430
it.ciphertext = ciphertext
@@ -41,7 +47,7 @@ class MessageV2Builder {
4147
topic = decryptedMessage.topic,
4248
encodedContent = decryptedMessage.encodedContent,
4349
senderAddress = decryptedMessage.senderAddress,
44-
sent = decryptedMessage.sentAt
50+
sent = decryptedMessage.sentAt,
4551
)
4652
} catch (e: Exception) {
4753
throw XMTPException("Error decoding message", e)
@@ -69,7 +75,7 @@ class MessageV2Builder {
6975

7076
if (!senderPreKey.signature.verify(
7177
senderIdentityKey,
72-
signed.sender.preKey.keyBytes.toByteArray()
78+
signed.sender.preKey.keyBytes.toByteArray(),
7379
)
7480
) {
7581
throw XMTPException("pre-key not signed by identity key")
@@ -109,7 +115,7 @@ class MessageV2Builder {
109115
encodedContent = encodedMessage,
110116
senderAddress = signed.sender.walletAddress,
111117
sentAt = Date(header.createdNs / 1_000_000),
112-
topic = topic
118+
topic = topic,
113119
)
114120
}
115121

@@ -118,6 +124,7 @@ class MessageV2Builder {
118124
encodedContent: EncodedContent,
119125
topic: String,
120126
keyMaterial: ByteArray,
127+
shouldPush: Boolean,
121128
): MessageV2 {
122129
val payload = encodedContent.toByteArray()
123130
val date = Date()
@@ -130,7 +137,14 @@ class MessageV2Builder {
130137
val signedContent = SignedContentBuilder.builderFromPayload(payload, bundle, signature)
131138
val signedBytes = signedContent.toByteArray()
132139
val ciphertext = Crypto.encrypt(keyMaterial, signedBytes, additionalData = headerBytes)
133-
return buildFromCipherText(headerBytes, ciphertext)
140+
141+
val thirtyDayPeriodsSinceEpoch =
142+
(System.currentTimeMillis() / 60 / 60 / 24 / 30).toInt()
143+
val info = "$thirtyDayPeriodsSinceEpoch-${client.address}"
144+
val infoEncoded = info.toByteStringUtf8().toByteArray()
145+
val senderHmac = Crypto.generateHmacSignature(keyMaterial, infoEncoded, headerBytes)
146+
147+
return buildFromCipherText(headerBytes, ciphertext, senderHmac, shouldPush)
134148
}
135149
}
136150
}

0 commit comments

Comments
 (0)