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

Add sender HMAC to MessageV2 #164

Merged
merged 31 commits into from
Mar 1, 2024
Merged
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
25664a0
Adding the way for adding should push and hmac
giovas17 Jan 26, 2024
bda8a7a
Adding new approach for codecs and crypto
giovas17 Jan 31, 2024
8286a3f
Solving test for conversation using new flag
giovas17 Jan 31, 2024
4001857
Update ConversationV2.kt
giovas17 Jan 31, 2024
fd117ab
do kotlin way of extending proto class
nplasterer Feb 1, 2024
92a5712
Adding the proper way to manage the codec changes
giovas17 Feb 8, 2024
97ea0c0
Update GroupMembershipChangeCodec.kt
giovas17 Feb 8, 2024
696e229
Update library/src/main/java/org/xmtp/android/library/Crypto.kt
nplasterer Feb 10, 2024
241992a
Update library/src/main/java/org/xmtp/android/library/Crypto.kt
nplasterer Feb 10, 2024
33d50f3
Update library/src/main/java/org/xmtp/android/library/Crypto.kt
nplasterer Feb 10, 2024
fd46f81
add method to get the hmac keys
nplasterer Feb 10, 2024
deec4b0
fix up the crypto
nplasterer Feb 10, 2024
c985c6a
add the get keys code for hmacs
nplasterer Feb 13, 2024
2127f77
remove unneeded crypto code
nplasterer Feb 13, 2024
ca3416c
write a test for it
nplasterer Feb 13, 2024
dd29da4
get the test to pass
nplasterer Feb 20, 2024
866b5a3
feat: integrate `shouldPush' for React Native (#184)
Feb 20, 2024
0279d00
Update GroupMembershipChangeTest.kt
giovas17 Feb 19, 2024
578e4b9
Removing issues and failing tests
giovas17 Feb 20, 2024
c5d97df
Update GroupMembershipChangeTest.kt
giovas17 Feb 20, 2024
50a413c
Updating shouldPush flag and removing issues in instrumental testing
giovas17 Feb 20, 2024
2f67cef
Ignoring failing tests
giovas17 Feb 20, 2024
2763b7e
remove all the ignored tests
nplasterer Feb 21, 2024
e113e3b
feat: add shouldPush property to MessageV2Builder
Feb 20, 2024
df578cf
fix up the lint issue
nplasterer Feb 21, 2024
f37d913
fix: deriveKey function and improvements
Feb 28, 2024
7f9394c
fix: getHmacKeys method
Feb 28, 2024
b04fc1e
fix: CodecText.kt
Feb 28, 2024
6f14b68
Revert "feat: integrate `shouldPush' for React Native (#184)"
Feb 28, 2024
5050c30
Revert "Removing issues and failing tests"
Feb 28, 2024
5bb1b5a
Remove @Ignore annotations from GroupTest
Mar 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Adding new approach for codecs and crypto
  • Loading branch information
giovas17 authored and kele-leanes committed Feb 28, 2024
commit bda8a7a1593b7f2834036902ff56dbae1df7a312
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ data class NumberCodec(
override fun decode(content: EncodedContent): Double =
content.content.toStringUtf8().filter { it.isDigit() || it == '.' }.toDouble()

override fun shouldPush(): Boolean = false
override fun shouldPush(content: Double): Boolean = false

override fun fallback(content: Double): String? {
return "Error: This app does not support numbers."
Original file line number Diff line number Diff line change
@@ -229,7 +229,12 @@ 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(),
@@ -585,6 +590,7 @@ class ConversationTest {
encodedContent,
topic = conversation.topic,
keyMaterial = conversation.keyMaterial!!,
shouldPush = true,
),
).toByteArray(),
),
Original file line number Diff line number Diff line change
@@ -91,7 +91,8 @@ class MessageTest {
client = client,
encodedContent,
topic = invitationv1.topic,
keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray()
keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray(),
shouldPush = true,
)
val decoded = MessageV2Builder.buildDecode(
id = "",
Original file line number Diff line number Diff line change
@@ -162,7 +162,11 @@ sealed class Conversation {
}

is V2 -> {
conversationV2.prepareMessage(encodedContent = encodedContent, options = options)
conversationV2.prepareMessage(
encodedContent = encodedContent,
options = options,
false,
)
}

is Group -> throw XMTPException("Groups do not support prepared messages") // We return a encoded content not a preparedmessage which requires a envelope
40 changes: 30 additions & 10 deletions library/src/main/java/org/xmtp/android/library/ConversationV2.kt
Original file line number Diff line number Diff line change
@@ -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,
)
}

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

fun send(encodedContent: EncodedContent, options: SendOptions?): String {
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
val codec = Client.codecRegistry.find(options?.contentType)
val preparedMessage = prepareMessage(
encodedContent = encodedContent,
options = options,
shouldPush = shouldPush(codec, encodedContent.content),
)
return send(preparedMessage)
}

@@ -202,16 +207,26 @@ data class ConversationV2(
client = client,
encodedContent = encodedContent,
topic = topic,
keyMaterial = keyMaterial
keyMaterial = keyMaterial,
shouldPush = shouldPush(codec, content),
)
val envelope = EnvelopeBuilder.buildFromString(
topic = topic,
timestamp = Date(),
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray(),
)
return envelope.toByteArray()
}

fun <Codec : ContentCodec<T>, T> shouldPush(codec: Codec, content: Any?): Boolean {
val contentType = content as? T
if (contentType != null) {
return codec.shouldPush(content = content)
} else {
throw XMTPException("Codec invalid content")
}
}

fun <T> prepareMessage(content: T, options: SendOptions?): PreparedMessage {
val codec = Client.codecRegistry.find(options?.contentType)

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

fun prepareMessage(encodedContent: EncodedContent, options: SendOptions?): PreparedMessage {
fun prepareMessage(
encodedContent: EncodedContent,
options: SendOptions?,
shouldPush: Boolean,
): PreparedMessage {
val message = MessageV2Builder.buildEncode(
client = client,
encodedContent = encodedContent,
topic = topic,
keyMaterial = keyMaterial
keyMaterial = keyMaterial,
shouldPush = shouldPush,
)

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

val envelope = EnvelopeBuilder.buildFromString(
topic = newTopic,
timestamp = Date(),
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray(),
)
return PreparedMessage(listOf(envelope))
}
Original file line number Diff line number Diff line change
@@ -37,5 +37,5 @@ data class AttachmentCodec(override var contentType: ContentTypeId = ContentType
return "Can’t display \"${content.filename}”. This app doesn’t support attachments."
}

override fun shouldPush(): Boolean = true
override fun shouldPush(content: Attachment): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ class CompositeCodec : ContentCodec<DecodedComposite> {
return null
}

override fun shouldPush(): Boolean = false
override fun shouldPush(content: DecodedComposite): Boolean = false

private fun toComposite(decodedComposite: DecodedComposite): Composite {
return Composite.newBuilder().also {
Original file line number Diff line number Diff line change
@@ -67,7 +67,7 @@ interface ContentCodec<T> {
fun encode(content: T): EncodedContent
fun decode(content: EncodedContent): T
fun fallback(content: T): String?
fun shouldPush(): Boolean
fun shouldPush(content: T): Boolean
}

val id: String
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ val ContentTypeReaction = ContentTypeIdBuilder.builderFromAuthorityId(
"xmtp.org",
"reaction",
versionMajor = 1,
versionMinor = 0
versionMinor = 0,
)

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

override fun shouldPush(): Boolean = true
override fun shouldPush(content: Reaction): Boolean = when (content.action) {
ReactionAction.Added -> true
ReactionAction.Removed -> false
ReactionAction.Unknown -> false
}
}

private class ReactionSerializer : JsonSerializer<Reaction> {
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ val ContentTypeReadReceipt = ContentTypeIdBuilder.builderFromAuthorityId(
"xmtp.org",
"readReceipt",
versionMajor = 1,
versionMinor = 0
versionMinor = 0,
)

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

override fun shouldPush(): Boolean = false
override fun shouldPush(content: ReadReceipt): Boolean = false
}
Original file line number Diff line number Diff line change
@@ -80,8 +80,10 @@ data class RemoteAttachment(
fun <T> encodeEncrypted(content: T, codec: ContentCodec<T>): 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<RemoteAttachment> {
data class RemoteAttachmentCodec(override var contentType: ContentTypeId = ContentTypeRemoteAttachment) :
ContentCodec<RemoteAttachment> {
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")

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

override fun shouldPush(): Boolean = true
override fun shouldPush(content: RemoteAttachment): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ val ContentTypeReply = ContentTypeIdBuilder.builderFromAuthorityId(
"xmtp.org",
"reply",
versionMajor = 1,
versionMinor = 0
versionMinor = 0,
)

data class Reply(
@@ -41,15 +41,15 @@ data class ReplyCodec(override var contentType: ContentTypeId = ContentTypeReply
return Reply(
reference = reference,
content = replyContent,
contentType = replyCodec.contentType
contentType = replyCodec.contentType,
)
}

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

override fun shouldPush(): Boolean = true
override fun shouldPush(content: Reply): Boolean = true

private fun <Codec : ContentCodec<T>, T> encodeReply(
codec: Codec,
Original file line number Diff line number Diff line change
@@ -37,5 +37,5 @@ data class TextCodec(override var contentType: ContentTypeId = ContentTypeText)
return null
}

override fun shouldPush(): Boolean = true
override fun shouldPush(content: String): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -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
@@ -18,7 +19,12 @@ typealias MessageV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV

class MessageV2Builder {
companion object {
fun buildFromCipherText(headerBytes: ByteArray, ciphertext: CipherText?): MessageV2 {
fun buildFromCipherText(
headerBytes: ByteArray,
ciphertext: CipherText?,
senderHmac: ByteArray?,
shouldPush: Boolean,
): MessageV2 {
return MessageV2.newBuilder().also {
it.headerBytes = headerBytes.toByteString()
it.ciphertext = ciphertext
@@ -41,7 +47,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 +75,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,7 +115,7 @@ class MessageV2Builder {
encodedContent = encodedMessage,
senderAddress = signed.sender.walletAddress,
sentAt = Date(header.createdNs / 1_000_000),
topic = topic
topic = topic,
)
}

@@ -118,6 +124,7 @@ class MessageV2Builder {
encodedContent: EncodedContent,
topic: String,
keyMaterial: ByteArray,
shouldPush: Boolean,
): MessageV2 {
val payload = encodedContent.toByteArray()
val date = Date()
@@ -130,7 +137,14 @@ 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 =
(System.currentTimeMillis() / 60 / 60 / 24 / 30).toInt()
val info = "$thirtyDayPeriodsSinceEpoch-${client.address}"
val infoEncoded = info.toByteStringUtf8().toByteArray()
val senderHmac = Crypto.generateHmacSignature(keyMaterial, infoEncoded, headerBytes)

return buildFromCipherText(headerBytes, ciphertext, senderHmac, shouldPush)
}
}
}
Loading