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

V3 HMAC key support for self push notifications #356

Merged
merged 15 commits into from
Jan 7, 2025
17 changes: 14 additions & 3 deletions example/src/main/java/org/xmtp/android/example/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@ class MainViewModel : ViewModel() {
val listItems = mutableListOf<MainListItem>()
try {
val conversations = ClientManager.client.conversations.list()
val subscriptions: MutableList<Service.Subscription> = conversations.map {
val subscriptions = conversations.map {
val hmacKeysResult = ClientManager.client.conversations.getHmacKeys()
val hmacKeys = hmacKeysResult.hmacKeysMap
val result = hmacKeys[it.topic]?.valuesList?.map { hmacKey ->
Service.Subscription.HmacKey.newBuilder().also { sub_key ->
sub_key.key = hmacKey.hmacKey
sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch
}.build()
}

Service.Subscription.newBuilder().also { sub ->
sub.addAllHmacKeys(result)
sub.topic = it.topic
sub.isSilent = false
}.build()
}.toMutableList()

Expand Down Expand Up @@ -85,7 +96,7 @@ class MainViewModel : ViewModel() {

@WorkerThread
private fun fetchMostRecentMessage(conversation: Conversation): Message? {
return runBlocking { conversation.messages(limit = 1).firstOrNull() }
return runBlocking { conversation.lastMessage() }
}

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -124,7 +135,7 @@ class MainViewModel : ViewModel() {
data class ConversationItem(
override val id: String,
val conversation: Conversation,
val mostRecentMessage: DecodedMessage?,
val mostRecentMessage: Message?,
) : MainListItem(id, ITEM_TYPE_CONVERSATION)

data class Footer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.xmtp.android.example.ClientManager
import org.xmtp.android.example.extension.flowWhileShared
import org.xmtp.android.example.extension.stateFlow
import org.xmtp.android.library.Conversation
import org.xmtp.android.library.libxmtp.Message

class ConversationDetailViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

Expand Down Expand Up @@ -125,7 +126,7 @@ class ConversationDetailViewModel(private val savedStateHandle: SavedStateHandle
const val ITEM_TYPE_MESSAGE = 1
}

data class Message(override val id: String, val message: DecodedMessage) :
data class Message(override val id: String, val message: org.xmtp.android.library.libxmtp.Message) :
MessageListItem(id, ITEM_TYPE_MESSAGE)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class MessageViewHolder(
if (item.message.content<Any>() is String) {
binding.messageBody.text = item.message.body
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
binding.messageDate.text = sdf.format(item.message.sent)
binding.messageDate.text = sdf.format(item.message.sentAt)

} else if (item.message.content<Any>() is GroupUpdated) {
val changes = item.message.content() as? GroupUpdated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ class PushNotificationsService : FirebaseMessagingService() {
return
}
val decodedMessage =
runBlocking { conversation.processMessage(encryptedMessageData).decode() }
runBlocking { conversation.processMessage(encryptedMessageData) }
val peerAddress = conversation.id

val body: String = if (decodedMessage.content<Any>() is String) {
val body: String = if (decodedMessage?.content<Any>() is String) {
decodedMessage.body
} else if (decodedMessage.content<Any>() is GroupUpdated) {
} else if (decodedMessage?.content<Any>() is GroupUpdated) {
val changes = decodedMessage.content() as? GroupUpdated
"Membership Changed ${
changes?.addedInboxesList?.mapNotNull { it.inboxId }.toString()
Expand Down
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ dependencies {
implementation 'org.web3j:crypto:4.9.4'
implementation "net.java.dev.jna:jna:5.14.0@aar"
api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
api 'org.xmtp:proto-kotlin:3.72.3'
api 'org.xmtp:proto-kotlin:3.72.4'

testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:monitor:1.7.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -187,4 +188,24 @@ class ConversationsTest {
assertEquals(2, allMessages.size)
job.cancel()
}

@Test
fun testReturnsAllHMACKeys() {
val conversations = mutableListOf<Conversation>()
repeat(5) {
val account = PrivateKeyBuilder()
val client = runBlocking { Client().create(account, fixtures.clientOptions) }
runBlocking {
conversations.add(
alixClient.conversations.newConversation(client.address)
)
}
}
val hmacKeys = alixClient.conversations.getHmacKeys()

val topics = hmacKeys.hmacKeysMap.keys
conversations.forEach { convo ->
assertTrue(topics.contains(convo.topic))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class HistorySyncTest {

runBlocking {
alixClient2.preferences.syncConsent()
Thread.sleep(2000)
alixClient.conversations.syncAllConversations()
Thread.sleep(2000)
alixClient2.conversations.syncAllConversations()
Expand Down Expand Up @@ -125,8 +124,7 @@ class HistorySyncTest {
runBlocking {
alix2Group.send("A message")
alix2Group.send("A second message")
alixClient3.requestMessageHistorySync()
Thread.sleep(1000)
Thread.sleep(2000)
alixClient.conversations.syncAllConversations()
Thread.sleep(2000)
alixClient2.conversations.syncAllConversations()
Expand Down Expand Up @@ -170,4 +168,45 @@ class HistorySyncTest {
assertEquals(alixGroup.consentState(), ConsentState.DENIED)
job.cancel()
}

@Test
fun testStreamPreferenceUpdates() {
var preferences = 0
val job = CoroutineScope(Dispatchers.IO).launch {
try {
alixClient.preferences.streamPreferenceUpdates()
.collect { entry ->
preferences++
}
} catch (e: Exception) {
}
}

Thread.sleep(2000)

runBlocking {
val alixClient3 = runBlocking {
Client().create(
account = alixWallet,
options = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, false),
appContext = fixtures.context,
dbEncryptionKey = fixtures.key,
dbDirectory = File(fixtures.context.filesDir.absolutePath, "xmtp_db3").toPath()
.toString()
)
)
}
alixClient3.conversations.syncAllConversations()
Thread.sleep(2000)
alixClient.conversations.syncAllConversations()
Thread.sleep(2000)
alixClient2.conversations.syncAllConversations()
Thread.sleep(2000)
}

Thread.sleep(2000)
assertEquals(2, preferences)
job.cancel()
}
}
5 changes: 0 additions & 5 deletions library/src/main/java/org/xmtp/android/library/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.xmtp.android.library.libxmtp.InboxState
import org.xmtp.android.library.libxmtp.Message
import org.xmtp.android.library.messages.rawData
import uniffi.xmtpv3.FfiConversationType
import uniffi.xmtpv3.FfiDeviceSyncKind
import uniffi.xmtpv3.FfiSignatureRequest
import uniffi.xmtpv3.FfiXmtpClient
import uniffi.xmtpv3.XmtpApiClient
Expand Down Expand Up @@ -386,10 +385,6 @@ class Client() {
ffiClient.dbReconnect()
}

suspend fun requestMessageHistorySync() {
ffiClient.sendSyncRequest(FfiDeviceSyncKind.MESSAGES)
}

suspend fun inboxStatesForInboxIds(
refreshFromNetwork: Boolean,
inboxIds: List<String>,
Expand Down
22 changes: 22 additions & 0 deletions library/src/main/java/org/xmtp/android/library/Conversations.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.xmtp.android.library

import android.util.Log
import com.google.protobuf.kotlin.toByteString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration
import org.xmtp.android.library.libxmtp.Message
import org.xmtp.android.library.messages.Topic
import org.xmtp.proto.keystore.api.v1.Keystore
import org.xmtp.android.library.libxmtp.PermissionPolicySet
import uniffi.xmtpv3.FfiConversation
import uniffi.xmtpv3.FfiConversationCallback
Expand Down Expand Up @@ -277,4 +280,23 @@ data class Conversations(

awaitClose { stream.end() }
}

fun getHmacKeys(): Keystore.GetConversationHmacKeysResponse {
val hmacKeysResponse = Keystore.GetConversationHmacKeysResponse.newBuilder()
val conversations = ffiConversations.getHmacKeys()
conversations.iterator().forEach {
val hmacKeys = Keystore.GetConversationHmacKeysResponse.HmacKeys.newBuilder()
it.value.forEach { key ->
val hmacKeyData = Keystore.GetConversationHmacKeysResponse.HmacKeyData.newBuilder()
hmacKeyData.hmacKey = key.key.toByteString()
hmacKeyData.thirtyDayPeriodsSinceEpoch = key.epoch.toInt()
hmacKeys.addValues(hmacKeyData)
}
hmacKeysResponse.putHmacKeys(
Topic.groupMessage(it.key.toHex()).description,
hmacKeys.build()
)
}
return hmacKeysResponse.build()
}
}
31 changes: 0 additions & 31 deletions library/src/main/java/org/xmtp/android/library/Crypto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import org.xmtp.proto.message.contents.CiphertextOuterClass
import java.security.GeneralSecurityException
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

Expand Down Expand Up @@ -74,35 +73,5 @@ 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 deriveKey(
secret: ByteArray,
salt: ByteArray,
info: ByteArray,
): ByteArray {
return Hkdf.computeHkdf("HMACSHA256", secret, salt, info, 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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import uniffi.xmtpv3.FfiConsentCallback
import uniffi.xmtpv3.FfiConsentEntityType
import uniffi.xmtpv3.FfiConsentState
import uniffi.xmtpv3.FfiDeviceSyncKind
import uniffi.xmtpv3.FfiPreferenceCallback
import uniffi.xmtpv3.FfiPreferenceUpdate
import uniffi.xmtpv3.FfiSubscribeException
import uniffi.xmtpv3.FfiXmtpClient

Expand Down Expand Up @@ -60,6 +62,10 @@ enum class EntryType {
}
}

enum class PreferenceType {
HMAC_KEYS;
}

data class ConsentRecord(
val value: String,
val entryType: EntryType,
Expand Down Expand Up @@ -100,6 +106,26 @@ data class PrivatePreferences(
ffiClient.sendSyncRequest(FfiDeviceSyncKind.CONSENT)
}

suspend fun streamPreferenceUpdates(): Flow<PreferenceType> = callbackFlow {
val preferenceCallback = object : FfiPreferenceCallback {
override fun onPreferenceUpdate(preference: List<FfiPreferenceUpdate>) {
preference.iterator().forEach {
when (it) {
is FfiPreferenceUpdate.Hmac -> trySend(PreferenceType.HMAC_KEYS)
}
}
}

override fun onError(error: FfiSubscribeException) {
Log.e("XMTP preference update stream", error.message.toString())
}
}

val stream = ffiClient.conversations().streamPreferences(preferenceCallback)

awaitClose { stream.end() }
}

suspend fun streamConsent(): Flow<ConsentRecord> = callbackFlow {
val consentCallback = object : FfiConsentCallback {
override fun onConsentUpdate(consent: List<FfiConsent>) {
Expand Down
Loading