diff --git a/dev/local/docker-compose.yml b/dev/local/docker-compose.yml index 197ba1d63..262f9d7f3 100644 --- a/dev/local/docker-compose.yml +++ b/dev/local/docker-compose.yml @@ -1,7 +1,7 @@ -version: "3.8" services: - wakunode: + waku-node: image: xmtp/node-go:latest + platform: linux/amd64 environment: - GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67 command: @@ -12,22 +12,10 @@ services: - --api.authn.enable ports: - 9001:9001 - - 5555:5555 # http message API - - 5556:5556 # grpc message API + - 5555:5555 depends_on: - db - healthcheck: - test: [ "CMD", "lsof", "-i", ":5556" ] - interval: 3s - timeout: 10s - retries: 5 db: image: postgres:13 environment: - POSTGRES_PASSWORD: xmtp - js: - restart: always - depends_on: - wakunode: - condition: service_healthy - build: ./test + POSTGRES_PASSWORD: xmtp \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 0152d55b7..bfbe72be0 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -12,6 +12,12 @@ dokkaGfmPartial { outputDirectory.set(file("build/docs/partial")) } +ktlint { + filter { + exclude { it.file.path.contains("xmtp_dh") } + } +} + android { namespace 'org.xmtp.android.library' compileSdk 33 @@ -80,7 +86,7 @@ dependencies { implementation 'org.web3j:crypto:5.0.0' implementation "net.java.dev.jna:jna:5.13.0@aar" api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3' - api 'org.xmtp:proto-kotlin:3.24.1' + api 'org.xmtp:proto-kotlin:3.31.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'app.cash.turbine:turbine:0.12.1' diff --git a/library/src/test/java/org/xmtp/android/library/ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt similarity index 74% rename from library/src/test/java/org/xmtp/android/library/ClientTest.kt rename to library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt index af29c0cc4..c438e48c6 100644 --- a/library/src/test/java/org/xmtp/android/library/ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt @@ -1,9 +1,15 @@ package org.xmtp.android.library +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.xmtp.android.library.messages.PrivateKeyBuilder +import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder +import org.xmtp.android.library.messages.generate +import org.xmtp.proto.message.contents.PrivateKeyOuterClass +@RunWith(AndroidJUnit4::class) class ClientTest { @Test fun testTakesAWallet() { @@ -20,6 +26,20 @@ class ClientTest { assert(preKey?.publicKey?.hasSignature() ?: false) } + @Test + fun testSerialization() { + val wallet = PrivateKeyBuilder() + val v1 = + PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet) + val encodedData = PrivateKeyBundleV1Builder.encodeData(v1) + val v1Copy = PrivateKeyBundleV1Builder.fromEncodedData(encodedData) + val client = Client().buildFrom(v1Copy) + assertEquals( + wallet.address, + client.address + ) + } + @Test fun testCanBeCreatedWithBundle() { val fakeWallet = PrivateKeyBuilder() diff --git a/library/src/test/java/org/xmtp/android/library/ContactsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt similarity index 61% rename from library/src/test/java/org/xmtp/android/library/ContactsTest.kt rename to library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt index a75b3bb37..3ccf68486 100644 --- a/library/src/test/java/org/xmtp/android/library/ContactsTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt @@ -1,9 +1,11 @@ package org.xmtp.android.library +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.xmtp.android.library.messages.walletAddress - +@RunWith(AndroidJUnit4::class) class ContactsTest { @Test @@ -35,4 +37,34 @@ class ContactsTest { } assert(fixtures.aliceClient.contacts.has(fixtures.bob.walletAddress)) } + + @Test + fun testAllowAddress() { + val fixtures = fixtures() + + val contacts = fixtures.bobClient.contacts + var result = contacts.isAllowed(fixtures.alice.walletAddress) + + assert(!result) + + contacts.allow(listOf(fixtures.alice.walletAddress)) + + result = contacts.isAllowed(fixtures.alice.walletAddress) + assert(result) + } + + @Test + fun testBlockAddress() { + val fixtures = fixtures() + + val contacts = fixtures.bobClient.contacts + var result = contacts.isAllowed(fixtures.alice.walletAddress) + + assert(!result) + + contacts.block(listOf(fixtures.alice.walletAddress)) + + result = contacts.isBlocked(fixtures.alice.walletAddress) + assert(result) + } } 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 1e0e3fe6f..2195ee675 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt @@ -713,4 +713,34 @@ class ConversationTest { assertEquals(1, messages.size) assertEquals("hi", messages[0].content()) } + + @Test + fun testCanHaveAllowState() { + val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null) + val isAllowed = bobConversation.allowState() == AllowState.ALLOW + + // Conversations you start should start as allowed + assertTrue(isAllowed) + + val aliceConversation = aliceClient.conversations.list()[0] + val isUnknown = aliceConversation.allowState() == AllowState.UNKNOWN + + // Conversations started with you should start as unknown + assertTrue(isUnknown) + + aliceClient.contacts.allow(listOf(bob.walletAddress)) + + val isBobAllowed = aliceConversation.allowState() == AllowState.ALLOW + assertTrue(isBobAllowed) + + val aliceClient2 = Client().create(aliceWallet, fakeApiClient) + val aliceConversation2 = aliceClient2.conversations.list()[0] + + aliceClient2.contacts.refreshAllowList() + + // Allow state should sync across clients + val isBobAllowed2 = aliceConversation2.allowState() == AllowState.ALLOW + + assertTrue(isBobAllowed2) + } } diff --git a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt index 929192efd..416f96dda 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt @@ -277,7 +277,7 @@ class LocalInstrumentedTest { @OptIn(ExperimentalCoroutinesApi::class) @Test - fun testStreamAllMessagesWorksWithIntros() = runBlocking { + fun testStreamAllMessagesWorksWithIntros() { val bob = PrivateKeyBuilder() val alice = PrivateKeyBuilder() val clientOptions = diff --git a/library/src/main/java/org/xmtp/android/library/Contacts.kt b/library/src/main/java/org/xmtp/android/library/Contacts.kt index b18fb2f23..8bb75e923 100644 --- a/library/src/main/java/org/xmtp/android/library/Contacts.kt +++ b/library/src/main/java/org/xmtp/android/library/Contacts.kt @@ -3,15 +3,161 @@ package org.xmtp.android.library import kotlinx.coroutines.runBlocking import org.xmtp.android.library.messages.ContactBundle import org.xmtp.android.library.messages.ContactBundleBuilder +import org.xmtp.android.library.messages.EnvelopeBuilder import org.xmtp.android.library.messages.Topic import org.xmtp.android.library.messages.walletAddress +import org.xmtp.proto.message.contents.PrivatePreferences.PrivatePreferencesAction +import java.util.Date + +typealias MessageType = PrivatePreferencesAction.MessageTypeCase + +enum class AllowState { + ALLOW, + BLOCK, + UNKNOWN +} +data class AllowListEntry( + val value: String, + val entryType: EntryType, + val permissionType: AllowState, +) { + enum class EntryType { + ADDRESS + } + + companion object { + fun address( + address: String, + type: AllowState = AllowState.UNKNOWN, + ): AllowListEntry { + return AllowListEntry(address, EntryType.ADDRESS, type) + } + } + + val key: String + get() = "${entryType.name}-$value" +} + +class AllowList(val client: Client) { + private val entries: MutableMap = mutableMapOf() + private val publicKey = + client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes + private val privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes + + @OptIn(ExperimentalUnsignedTypes::class) + private val identifier: String = uniffi.xmtp_dh.generatePrivatePreferencesTopicIdentifier( + privateKey.toByteArray().toUByteArray().toList() + ) + + @OptIn(ExperimentalUnsignedTypes::class) + suspend fun load(): AllowList { + val envelopes = client.query(Topic.preferenceList(identifier)) + val allowList = AllowList(client) + val preferences: MutableList = mutableListOf() + + for (envelope in envelopes.envelopesList) { + val payload = uniffi.xmtp_dh.eciesDecryptK256Sha3256( + publicKey.toByteArray().toUByteArray().toList(), + privateKey.toByteArray().toUByteArray().toList(), + envelope.message.toByteArray().toUByteArray().toList() + ) + + preferences.add( + PrivatePreferencesAction.parseFrom( + payload.toUByteArray().toByteArray() + ) + ) + } + + preferences.iterator().forEach { preference -> + preference.allow?.walletAddressesList?.forEach { address -> + allowList.allow(address) + } + preference.block?.walletAddressesList?.forEach { address -> + allowList.block(address) + } + } + return allowList + } + + @OptIn(ExperimentalUnsignedTypes::class) + fun publish(entry: AllowListEntry) { + val payload = PrivatePreferencesAction.newBuilder().also { + when (entry.permissionType) { + AllowState.ALLOW -> it.setAllow(PrivatePreferencesAction.Allow.newBuilder().addWalletAddresses(entry.value)) + AllowState.BLOCK -> it.setBlock(PrivatePreferencesAction.Block.newBuilder().addWalletAddresses(entry.value)) + AllowState.UNKNOWN -> it.clearMessageType() + } + }.build() + + val message = uniffi.xmtp_dh.eciesEncryptK256Sha3256( + publicKey.toByteArray().toUByteArray().toList(), + privateKey.toByteArray().toUByteArray().toList(), + payload.toByteArray().toUByteArray().toList() + ) + + val envelope = EnvelopeBuilder.buildFromTopic( + Topic.preferenceList(identifier), + Date(), + ByteArray(message.size) { message[it].toByte() } + ) + + client.publish(listOf(envelope)) + } + + fun allow(address: String): AllowListEntry { + entries[AllowListEntry.address(address).key] = AllowState.ALLOW + + return AllowListEntry.address(address, AllowState.ALLOW) + } + + fun block(address: String): AllowListEntry { + entries[AllowListEntry.address(address).key] = AllowState.BLOCK + + return AllowListEntry.address(address, AllowState.BLOCK) + } + + fun state(address: String): AllowState { + val state = entries[AllowListEntry.address(address).key] + + return state ?: AllowState.UNKNOWN + } +} data class Contacts( var client: Client, val knownBundles: MutableMap = mutableMapOf(), - val hasIntroduced: MutableMap = mutableMapOf() + val hasIntroduced: MutableMap = mutableMapOf(), ) { + var allowList: AllowList = AllowList(client) + + fun refreshAllowList() { + runBlocking { + allowList = AllowList(client).load() + } + } + + fun isAllowed(address: String): Boolean { + return allowList.state(address) == AllowState.ALLOW + } + + fun isBlocked(address: String): Boolean { + return allowList.state(address) == AllowState.BLOCK + } + + fun allow(addresses: List) { + for (address in addresses) { + AllowList(client).publish(allowList.allow(address)) + } + } + + fun block(addresses: List) { + for (address in addresses) { + AllowList(client).publish(allowList.block(address)) + } + } + fun has(peerAddress: String): Boolean = knownBundles[peerAddress] != null diff --git a/library/src/main/java/org/xmtp/android/library/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index 9f2fef936..9da904e1c 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -61,6 +61,14 @@ sealed class Conversation { } } + fun allowState(): AllowState { + val client: Client = when (this) { + is V1 -> conversationV1.client + is V2 -> conversationV2.client + } + return client.contacts.allowList.state(address = peerAddress) + } + fun toTopicData(): TopicData { val data = TopicData.newBuilder() .setCreatedNs(createdAt.time * 1_000_000) 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 84fbb88d8..45f244fcc 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -142,6 +142,7 @@ data class Conversations( invitation = invitation, header = sealedInvitation.v1.header ) + client.contacts.allow(addresses = listOf(peerAddress)) val conversation = Conversation.V2(conversationV2) conversationsByTopic[conversation.topic] = conversation return conversation diff --git a/library/src/main/java/org/xmtp/android/library/messages/Topic.kt b/library/src/main/java/org/xmtp/android/library/messages/Topic.kt index 004416531..debb175c9 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/Topic.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/Topic.kt @@ -7,7 +7,7 @@ sealed class Topic { data class userInvite(val address: String?) : Topic() data class directMessageV1(val address1: String?, val address2: String?) : Topic() data class directMessageV2(val addresses: String?) : Topic() - data class groupInvite(val address: String?) : Topic() + data class preferenceList(val identifier: String?) : Topic() val description: String get() { @@ -22,7 +22,7 @@ sealed class Topic { wrap("dm-${addresses.joinToString(separator = "-")}") } is directMessageV2 -> wrap("m-$addresses") - is groupInvite -> wrap("groupInvite-$address") + is preferenceList -> wrap("pppp-$identifier") } } diff --git a/library/src/main/java/xmtp_dh.kt b/library/src/main/java/xmtp_dh.kt index a0acd4948..b12d6cc5e 100644 --- a/library/src/main/java/xmtp_dh.kt +++ b/library/src/main/java/xmtp_dh.kt @@ -18,12 +18,14 @@ package uniffi.xmtp_dh // helpers directly inline like we're doing here. import com.sun.jna.Library +import com.sun.jna.IntegerType import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.Structure -import com.sun.jna.ptr.ByReference +import com.sun.jna.ptr.* import java.nio.ByteBuffer import java.nio.ByteOrder +import java.util.concurrent.ConcurrentHashMap // This is a helper for safely working with byte buffers returned from the Rust code. // A rust-owned buffer is represented by its capacity, its current length, and a @@ -45,15 +47,15 @@ open class RustBuffer : Structure() { companion object { internal fun alloc(size: Int = 0) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_xmtp_dh_ff4a_rustbuffer_alloc(size, status).also { + _UniFFILib.INSTANCE.ffi_xmtp_dh_rustbuffer_alloc(size, status).also { if (it.data == null) { - throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=$size)") + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") } } } internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> - _UniFFILib.INSTANCE.ffi_xmtp_dh_ff4a_rustbuffer_free(buf, status) + _UniFFILib.INSTANCE.ffi_xmtp_dh_rustbuffer_free(buf, status) } } @@ -81,6 +83,19 @@ class RustBufferByReference : ByReference(16) { pointer.setInt(4, value.len) pointer.setPointer(8, value.data) } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getInt(0)) + value.writeField("len", pointer.getInt(4)) + value.writeField("data", pointer.getPointer(8)) + + return value + } } // This is a helper for safely passing byte references into the rust code. @@ -178,21 +193,23 @@ public interface FfiConverterRustBuffer : FfiConverter { - fun lift(error_buf: RustBuffer.ByValue): E + fun lift(error_buf: RustBuffer.ByValue): E; } // Helpers for calling Rust @@ -212,10 +229,19 @@ private inline fun rustCallWithError( errorHandler: CallStatusErrorHandler, callback: (RustCallStatus) -> U, ): U { - var status = RustCallStatus() + var status = RustCallStatus(); val return_value = callback(status) + checkCallStatus(errorHandler, status) + return return_value +} + +// Check RustCallStatus and throw an error if the call wasn't successful +private fun checkCallStatus( + errorHandler: CallStatusErrorHandler, + status: RustCallStatus, +) { if (status.isSuccess()) { - return return_value + return } else if (status.isError()) { throw errorHandler.lift(status.error_buf) } else if (status.isPanic()) { @@ -242,7 +268,88 @@ object NullCallStatusErrorHandler : CallStatusErrorHandler { // Call a rust function that returns a plain value private inline fun rustCall(callback: (RustCallStatus) -> U): U { - return rustCallWithError(NullCallStatusErrorHandler, callback) + return rustCallWithError(NullCallStatusErrorHandler, callback); +} + +// IntegerType that matches Rust's `usize` / C's `size_t` +public class USize(value: Long = 0) : IntegerType(Native.SIZE_T_SIZE, value, true) { + // This is needed to fill in the gaps of IntegerType's implementation of Number for Kotlin. + override fun toByte() = toInt().toByte() + override fun toChar() = toInt().toChar() + override fun toShort() = toInt().toShort() + + fun writeToBuffer(buf: ByteBuffer) { + // Make sure we always write usize integers using native byte-order, since they may be + // casted to pointer values + buf.order(ByteOrder.nativeOrder()) + try { + when (Native.SIZE_T_SIZE) { + 4 -> buf.putInt(toInt()) + 8 -> buf.putLong(toLong()) + else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}") + } + } finally { + buf.order(ByteOrder.BIG_ENDIAN) + } + } + + companion object { + val size: Int + get() = Native.SIZE_T_SIZE + + fun readFromBuffer(buf: ByteBuffer): USize { + // Make sure we always read usize integers using native byte-order, since they may be + // casted from pointer values + buf.order(ByteOrder.nativeOrder()) + try { + return when (Native.SIZE_T_SIZE) { + 4 -> USize(buf.getInt().toLong()) + 8 -> USize(buf.getLong()) + else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}") + } + } finally { + buf.order(ByteOrder.BIG_ENDIAN) + } + } + } +} + + +// Map handles to objects +// +// This is used when the Rust code expects an opaque pointer to represent some foreign object. +// Normally we would pass a pointer to the object, but JNA doesn't support getting a pointer from an +// object reference , nor does it support leaking a reference to Rust. +// +// Instead, this class maps USize values to objects so that we can pass a pointer-sized type to +// Rust when it needs an opaque pointer. +// +// TODO: refactor callbacks to use this class +internal class UniFfiHandleMap { + private val map = ConcurrentHashMap() + + // Use AtomicInteger for our counter, since we may be on a 32-bit system. 4 billion possible + // values seems like enough. If somehow we generate 4 billion handles, then this will wrap + // around back to zero and we can assume the first handle generated will have been dropped by + // then. + private val counter = java.util.concurrent.atomic.AtomicInteger(0) + + val size: Int + get() = map.size + + fun insert(obj: T): USize { + val handle = USize(counter.getAndAdd(1).toLong()) + map.put(handle, obj) + return handle + } + + fun get(handle: USize): T? { + return map.get(handle) + } + + fun remove(handle: USize) { + map.remove(handle) + } } // Contains loading, initialization code, @@ -269,38 +376,99 @@ internal interface _UniFFILib : Library { companion object { internal val INSTANCE: _UniFFILib by lazy { loadIndirect<_UniFFILib>(componentName = "xmtp_dh") + .also { lib: _UniFFILib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } } } - fun xmtp_dh_ff4a_diffie_hellman_k256( + fun uniffi_xmtp_dh_fn_func_diffie_hellman_k256( `privateKeyBytes`: RustBuffer.ByValue, `publicKeyBytes`: RustBuffer.ByValue, _uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - fun ffi_xmtp_dh_ff4a_rustbuffer_alloc( - `size`: Int, + fun uniffi_xmtp_dh_fn_func_ecies_encrypt_k256_sha3_256( + `publicKeyBytes`: RustBuffer.ByValue, + `privateKeyBytes`: RustBuffer.ByValue, + `messageBytes`: RustBuffer.ByValue, _uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - fun ffi_xmtp_dh_ff4a_rustbuffer_from_bytes( - `bytes`: ForeignBytes.ByValue, + fun uniffi_xmtp_dh_fn_func_ecies_decrypt_k256_sha3_256( + `publicKeyBytes`: RustBuffer.ByValue, + `privateKeyBytes`: RustBuffer.ByValue, + `messageBytes`: RustBuffer.ByValue, _uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue - fun ffi_xmtp_dh_ff4a_rustbuffer_free( - `buf`: RustBuffer.ByValue, - _uniffi_out_err: RustCallStatus, + fun uniffi_xmtp_dh_fn_func_generate_private_preferences_topic_identifier( + `privateKeyBytes`: RustBuffer.ByValue, _uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + + fun ffi_xmtp_dh_rustbuffer_alloc( + `size`: Int, _uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + + fun ffi_xmtp_dh_rustbuffer_from_bytes( + `bytes`: ForeignBytes.ByValue, _uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + + fun ffi_xmtp_dh_rustbuffer_free( + `buf`: RustBuffer.ByValue, _uniffi_out_err: RustCallStatus, ): Unit - fun ffi_xmtp_dh_ff4a_rustbuffer_reserve( - `buf`: RustBuffer.ByValue, - `additional`: Int, - _uniffi_out_err: RustCallStatus, + fun ffi_xmtp_dh_rustbuffer_reserve( + `buf`: RustBuffer.ByValue, `additional`: Int, _uniffi_out_err: RustCallStatus, ): RustBuffer.ByValue + + fun uniffi_xmtp_dh_checksum_func_diffie_hellman_k256( + ): Short + + fun uniffi_xmtp_dh_checksum_func_ecies_encrypt_k256_sha3_256( + ): Short + + fun uniffi_xmtp_dh_checksum_func_ecies_decrypt_k256_sha3_256( + ): Short + + fun uniffi_xmtp_dh_checksum_func_generate_private_preferences_topic_identifier( + ): Short + + fun ffi_xmtp_dh_uniffi_contract_version( + ): Int + +} + +private fun uniffiCheckContractApiVersion(lib: _UniFFILib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 22 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_xmtp_dh_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} + +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: _UniFFILib) { + if (lib.uniffi_xmtp_dh_checksum_func_diffie_hellman_k256() != 64890.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_xmtp_dh_checksum_func_ecies_encrypt_k256_sha3_256() != 28010.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_xmtp_dh_checksum_func_ecies_decrypt_k256_sha3_256() != 45037.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_xmtp_dh_checksum_func_generate_private_preferences_topic_identifier() != 65141.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } } // Public interface members begin here. + + public object FfiConverterUByte : FfiConverter { override fun lift(value: Byte): UByte { return value.toUByte() @@ -367,11 +535,13 @@ public object FfiConverterString : FfiConverter { } } + sealed class DiffieHellmanException(message: String) : Exception(message) { // Each variant is a nested class // Flat enums carries a string error message, so no special implementation is necessary. class GenericException(message: String) : DiffieHellmanException(message) + companion object ErrorHandler : CallStatusErrorHandler { override fun lift(error_buf: RustBuffer.ByValue): DiffieHellmanException = FfiConverterTypeDiffieHellmanError.lift(error_buf) @@ -385,6 +555,7 @@ public object FfiConverterTypeDiffieHellmanError : FfiConverterRustBuffer DiffieHellmanException.GenericException(FfiConverterString.read(buf)) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } + } override fun allocationSize(value: DiffieHellmanException): Int { @@ -399,8 +570,48 @@ public object FfiConverterTypeDiffieHellmanError : FfiConverterRustBuffer { + override fun lift(error_buf: RustBuffer.ByValue): EciesException = + FfiConverterTypeEciesError.lift(error_buf) + } } +public object FfiConverterTypeEciesError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): EciesException { + + return when (buf.getInt()) { + 1 -> EciesException.GenericException(FfiConverterString.read(buf)) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + + } + + override fun allocationSize(value: EciesException): Int { + return 4 + } + + override fun write(value: EciesException, buf: ByteBuffer) { + when (value) { + is EciesException.GenericException -> { + buf.putInt(1) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + public object FfiConverterSequenceUByte : FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): List { val len = buf.getInt() @@ -431,13 +642,60 @@ fun `diffieHellmanK256`( ): List { return FfiConverterSequenceUByte.lift( rustCallWithError(DiffieHellmanException) { _status -> - _UniFFILib.INSTANCE.xmtp_dh_ff4a_diffie_hellman_k256( + _UniFFILib.INSTANCE.uniffi_xmtp_dh_fn_func_diffie_hellman_k256( FfiConverterSequenceUByte.lower( `privateKeyBytes` - ), + ), FfiConverterSequenceUByte.lower(`publicKeyBytes`), _status + ) + }) +} + +@Throws(EciesException::class) + +fun `eciesEncryptK256Sha3256`( + `publicKeyBytes`: List, + `privateKeyBytes`: List, + `messageBytes`: List, +): List { + return FfiConverterSequenceUByte.lift( + rustCallWithError(EciesException) { _status -> + _UniFFILib.INSTANCE.uniffi_xmtp_dh_fn_func_ecies_encrypt_k256_sha3_256( FfiConverterSequenceUByte.lower(`publicKeyBytes`), + FfiConverterSequenceUByte.lower(`privateKeyBytes`), + FfiConverterSequenceUByte.lower(`messageBytes`), _status ) - } - ) + }) +} + +@Throws(EciesException::class) + +fun `eciesDecryptK256Sha3256`( + `publicKeyBytes`: List, + `privateKeyBytes`: List, + `messageBytes`: List, +): List { + return FfiConverterSequenceUByte.lift( + rustCallWithError(EciesException) { _status -> + _UniFFILib.INSTANCE.uniffi_xmtp_dh_fn_func_ecies_decrypt_k256_sha3_256( + FfiConverterSequenceUByte.lower(`publicKeyBytes`), + FfiConverterSequenceUByte.lower(`privateKeyBytes`), + FfiConverterSequenceUByte.lower(`messageBytes`), + _status + ) + }) } + +@Throws(EciesException::class) + +fun `generatePrivatePreferencesTopicIdentifier`(`privateKeyBytes`: List): String { + return FfiConverterString.lift( + rustCallWithError(EciesException) { _status -> + _UniFFILib.INSTANCE.uniffi_xmtp_dh_fn_func_generate_private_preferences_topic_identifier( + FfiConverterSequenceUByte.lower(`privateKeyBytes`), + _status + ) + }) +} + + diff --git a/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtp_dh.so b/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtp_dh.so index 53599d237..d51256f36 100755 Binary files a/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtp_dh.so and b/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtp_dh.so differ diff --git a/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtp_dh.so b/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtp_dh.so index 3aa2201fb..9fb109c49 100755 Binary files a/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtp_dh.so and b/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtp_dh.so differ diff --git a/library/src/main/jniLibs/x86/libuniffi_xmtp_dh.so b/library/src/main/jniLibs/x86/libuniffi_xmtp_dh.so index 2939ef7ac..6c2379cd7 100755 Binary files a/library/src/main/jniLibs/x86/libuniffi_xmtp_dh.so and b/library/src/main/jniLibs/x86/libuniffi_xmtp_dh.so differ diff --git a/library/src/main/jniLibs/x86_64/libuniffi_xmtp_dh.so b/library/src/main/jniLibs/x86_64/libuniffi_xmtp_dh.so index c60b32b28..75d2e8294 100755 Binary files a/library/src/main/jniLibs/x86_64/libuniffi_xmtp_dh.so and b/library/src/main/jniLibs/x86_64/libuniffi_xmtp_dh.so differ diff --git a/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt b/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt index ca21aba74..3c08b00ee 100644 --- a/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt +++ b/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt @@ -3,7 +3,6 @@ package org.xmtp.android.library import org.junit.Assert.assertEquals import org.junit.Test import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder import org.xmtp.android.library.messages.UnsignedPublicKey import org.xmtp.android.library.messages.generate import org.xmtp.android.library.messages.getPublicKeyBundle @@ -26,20 +25,6 @@ class PrivateKeyBundleTest { ) } - @Test - fun testSerialization() { - val wallet = PrivateKeyBuilder() - val v1 = - PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet) - val encodedData = PrivateKeyBundleV1Builder.encodeData(v1) - val v1Copy = PrivateKeyBundleV1Builder.fromEncodedData(encodedData) - val client = Client().buildFrom(v1Copy) - assertEquals( - wallet.address, - client.address - ) - } - @Test fun testKeyBundlesAreSigned() { val wallet = PrivateKeyBuilder()