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 5c46ea650..154a7272d 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt @@ -341,6 +341,79 @@ class ClientTest { ) } + @Test + fun testsSignatures() { + val fixtures = fixtures() + val signature = fixtures.alixClient.signWithInstallationKey("Testing") + assertEquals(fixtures.alixClient.verifySignature("Testing", signature), true) + assertEquals(fixtures.alixClient.verifySignature("Not Testing", signature), false) + + val alixInstallationId = fixtures.alixClient.installationId + assertEquals( + fixtures.alixClient.verifySignatureWithInstallationId( + "Testing", + signature, + alixInstallationId + ), + true + ) + assertEquals( + fixtures.alixClient.verifySignatureWithInstallationId( + "Not Testing", + signature, + alixInstallationId + ), + false + ) + assertEquals( + fixtures.alixClient.verifySignatureWithInstallationId( + "Testing", + signature, + fixtures.boClient.installationId + ), + false + ) + assertEquals( + fixtures.boClient.verifySignatureWithInstallationId( + "Testing", + signature, + alixInstallationId + ), + true + ) + fixtures.alixClient.deleteLocalDatabase() + + val key = SecureRandom().generateSeed(32) + val context = InstrumentationRegistry.getInstrumentation().targetContext + val alixClient2 = runBlocking { + Client().create( + account = fixtures.alixAccount, + options = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, false), + appContext = context, + dbEncryptionKey = key + ) + ) + } + + assertEquals( + alixClient2.verifySignatureWithInstallationId( + "Testing", + signature, + alixInstallationId + ), + true + ) + assertEquals( + alixClient2.verifySignatureWithInstallationId( + "Testing2", + signature, + alixInstallationId + ), + false + ) + } + @Test fun testAddAccounts() { val fixtures = fixtures() diff --git a/library/src/main/java/libxmtp-version.txt b/library/src/main/java/libxmtp-version.txt index 4929643da..ceb459d88 100644 --- a/library/src/main/java/libxmtp-version.txt +++ b/library/src/main/java/libxmtp-version.txt @@ -1,3 +1,3 @@ -Version: 8e8ff6a6 +Version: cbff7296 Branch: main -Date: 2024-11-19 01:30:43 +0000 +Date: 2024-11-21 17:51:27 +0000 diff --git a/library/src/main/java/org/xmtp/android/library/Client.kt b/library/src/main/java/org/xmtp/android/library/Client.kt index 60deb0e1a..087b76d95 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -236,6 +236,24 @@ class Client() { return ffiClient.signWithInstallationKey(message) } + fun verifySignature(message: String, signature: ByteArray): Boolean { + return try { + ffiClient.verifySignedWithInstallationKey(message, signature) + true + } catch (e: Exception) { + false + } + } + + fun verifySignatureWithInstallationId(message: String, signature: ByteArray, installationId: String): Boolean { + return try { + ffiClient.verifySignedWithPublicKey(message, signature, installationId.hexToByteArray()) + true + } catch (e: Exception) { + false + } + } + fun findGroup(groupId: String): Group? { return try { Group(this, ffiClient.conversation(groupId.hexToByteArray())) diff --git a/library/src/main/java/xmtpv3.kt b/library/src/main/java/xmtpv3.kt index d9fe665d9..c023acb1a 100644 --- a/library/src/main/java/xmtpv3.kt +++ b/library/src/main/java/xmtpv3.kt @@ -737,6 +737,24 @@ internal interface UniffiCallbackInterfaceFfiLoggerMethod0 : com.sun.jna.Callbac ) } +internal interface UniffiCallbackInterfaceFfiConsentCallbackMethod0 : com.sun.jna.Callback { + fun callback( + `uniffiHandle`: Long, + `consent`: RustBuffer.ByValue, + `uniffiOutReturn`: Pointer, + uniffiCallStatus: UniffiRustCallStatus, + ) +} + +internal interface UniffiCallbackInterfaceFfiConsentCallbackMethod1 : com.sun.jna.Callback { + fun callback( + `uniffiHandle`: Long, + `error`: RustBuffer.ByValue, + `uniffiOutReturn`: Pointer, + uniffiCallStatus: UniffiRustCallStatus, + ) +} + internal interface UniffiCallbackInterfaceFfiConversationCallbackMethod0 : com.sun.jna.Callback { fun callback( `uniffiHandle`: Long, @@ -829,6 +847,30 @@ internal open class UniffiVTableCallbackInterfaceFfiLogger( } +@Structure.FieldOrder("onConsentUpdate", "onError", "uniffiFree") +internal open class UniffiVTableCallbackInterfaceFfiConsentCallback( + @JvmField internal var `onConsentUpdate`: UniffiCallbackInterfaceFfiConsentCallbackMethod0? = null, + @JvmField internal var `onError`: UniffiCallbackInterfaceFfiConsentCallbackMethod1? = null, + @JvmField internal var `uniffiFree`: UniffiCallbackInterfaceFree? = null, +) : Structure() { + class UniffiByValue( + `onConsentUpdate`: UniffiCallbackInterfaceFfiConsentCallbackMethod0? = null, + `onError`: UniffiCallbackInterfaceFfiConsentCallbackMethod1? = null, + `uniffiFree`: UniffiCallbackInterfaceFree? = null, + ) : UniffiVTableCallbackInterfaceFfiConsentCallback( + `onConsentUpdate`, + `onError`, + `uniffiFree`, + ), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiVTableCallbackInterfaceFfiConsentCallback) { + `onConsentUpdate` = other.`onConsentUpdate` + `onError` = other.`onError` + `uniffiFree` = other.`uniffiFree` + } + +} + @Structure.FieldOrder("onConversation", "onError", "uniffiFree") internal open class UniffiVTableCallbackInterfaceFfiConversationCallback( @JvmField internal var `onConversation`: UniffiCallbackInterfaceFfiConversationCallbackMethod0? = null, @@ -909,6 +951,7 @@ internal interface UniffiLib : Library { .also { lib: UniffiLib -> uniffiCheckContractApiVersion(lib) uniffiCheckApiChecksums(lib) + uniffiCallbackInterfaceFfiConsentCallback.register(lib) uniffiCallbackInterfaceFfiConversationCallback.register(lib) uniffiCallbackInterfaceFfiMessageCallback.register(lib) uniffiCallbackInterfaceFfiV2SubscriptionCallback.register(lib) @@ -923,6 +966,26 @@ internal interface UniffiLib : Library { } } + fun uniffi_xmtpv3_fn_clone_fficonsentcallback( + `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, + ): Pointer + + fun uniffi_xmtpv3_fn_free_fficonsentcallback( + `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, + ): Unit + + fun uniffi_xmtpv3_fn_init_callback_vtable_fficonsentcallback( + `vtable`: UniffiVTableCallbackInterfaceFfiConsentCallback, + ): Unit + + fun uniffi_xmtpv3_fn_method_fficonsentcallback_on_consent_update( + `ptr`: Pointer, `consent`: RustBuffer.ByValue, uniffi_out_err: UniffiRustCallStatus, + ): Unit + + fun uniffi_xmtpv3_fn_method_fficonsentcallback_on_error( + `ptr`: Pointer, `error`: RustBuffer.ByValue, uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_xmtpv3_fn_clone_fficonversation( `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, ): Pointer @@ -1142,6 +1205,10 @@ internal interface UniffiLib : Library { `ptr`: Pointer, `accountAddresses`: RustBuffer.ByValue, `opts`: RustBuffer.ByValue, ): Long + fun uniffi_xmtpv3_fn_method_fficonversations_get_sync_group( + `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_xmtpv3_fn_method_fficonversations_list( `ptr`: Pointer, `opts`: RustBuffer.ByValue, ): Long @@ -1174,6 +1241,10 @@ internal interface UniffiLib : Library { `ptr`: Pointer, `messageCallback`: Pointer, ): Long + fun uniffi_xmtpv3_fn_method_fficonversations_stream_consent( + `ptr`: Pointer, `callback`: Pointer, + ): Long + fun uniffi_xmtpv3_fn_method_fficonversations_stream_dms( `ptr`: Pointer, `callback`: Pointer, ): Long @@ -1456,6 +1527,21 @@ internal interface UniffiLib : Library { `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + fun uniffi_xmtpv3_fn_method_ffixmtpclient_verify_signed_with_installation_key( + `ptr`: Pointer, + `signatureText`: RustBuffer.ByValue, + `signatureBytes`: RustBuffer.ByValue, + uniffi_out_err: UniffiRustCallStatus, + ): Unit + + fun uniffi_xmtpv3_fn_method_ffixmtpclient_verify_signed_with_public_key( + `ptr`: Pointer, + `signatureText`: RustBuffer.ByValue, + `signatureBytes`: RustBuffer.ByValue, + `publicKey`: RustBuffer.ByValue, + uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_xmtpv3_fn_init_callback_vtable_ffiinboxowner( `vtable`: UniffiVTableCallbackInterfaceFfiInboxOwner, ): Unit @@ -1830,6 +1916,12 @@ internal interface UniffiLib : Library { fun uniffi_xmtpv3_checksum_func_verify_k256_sha256( ): Short + fun uniffi_xmtpv3_checksum_method_fficonsentcallback_on_consent_update( + ): Short + + fun uniffi_xmtpv3_checksum_method_fficonsentcallback_on_error( + ): Short + fun uniffi_xmtpv3_checksum_method_fficonversation_add_admin( ): Short @@ -1965,6 +2057,9 @@ internal interface UniffiLib : Library { fun uniffi_xmtpv3_checksum_method_fficonversations_create_group( ): Short + fun uniffi_xmtpv3_checksum_method_fficonversations_get_sync_group( + ): Short + fun uniffi_xmtpv3_checksum_method_fficonversations_list( ): Short @@ -1989,6 +2084,9 @@ internal interface UniffiLib : Library { fun uniffi_xmtpv3_checksum_method_fficonversations_stream_all_messages( ): Short + fun uniffi_xmtpv3_checksum_method_fficonversations_stream_consent( + ): Short + fun uniffi_xmtpv3_checksum_method_fficonversations_stream_dms( ): Short @@ -2142,6 +2240,12 @@ internal interface UniffiLib : Library { fun uniffi_xmtpv3_checksum_method_ffixmtpclient_signature_request( ): Short + fun uniffi_xmtpv3_checksum_method_ffixmtpclient_verify_signed_with_installation_key( + ): Short + + fun uniffi_xmtpv3_checksum_method_ffixmtpclient_verify_signed_with_public_key( + ): Short + fun uniffi_xmtpv3_checksum_method_ffiinboxowner_get_address( ): Short @@ -2216,6 +2320,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_xmtpv3_checksum_func_verify_k256_sha256() != 25521.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_xmtpv3_checksum_method_fficonsentcallback_on_consent_update() != 12532.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_xmtpv3_checksum_method_fficonsentcallback_on_error() != 5882.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_xmtpv3_checksum_method_fficonversation_add_admin() != 52417.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -2351,6 +2461,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_xmtpv3_checksum_method_fficonversations_create_group() != 7282.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_xmtpv3_checksum_method_fficonversations_get_sync_group() != 42973.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_xmtpv3_checksum_method_fficonversations_list() != 42790.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -2375,6 +2488,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_xmtpv3_checksum_method_fficonversations_stream_all_messages() != 63519.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_xmtpv3_checksum_method_fficonversations_stream_consent() != 12642.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_xmtpv3_checksum_method_fficonversations_stream_dms() != 52710.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -2528,6 +2644,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_xmtpv3_checksum_method_ffixmtpclient_signature_request() != 18270.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_xmtpv3_checksum_method_ffixmtpclient_verify_signed_with_installation_key() != 4340.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_xmtpv3_checksum_method_ffixmtpclient_verify_signed_with_public_key() != 21532.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_xmtpv3_checksum_method_ffiinboxowner_get_address() != 2205.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -2981,6 +3103,340 @@ private class JavaLangRefCleanable( override fun clean() = cleanable.clean() } +public interface FfiConsentCallback { + + fun `onConsentUpdate`(`consent`: List) + + fun `onError`(`error`: FfiSubscribeException) + + companion object +} + +open class FfiConsentCallbackImpl : Disposable, AutoCloseable, FfiConsentCallback { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (!this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_free_fficonsentcallback(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_clone_fficonsentcallback(pointer!!, status) + } + } + + override fun `onConsentUpdate`(`consent`: List) = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_fficonsentcallback_on_consent_update( + it, FfiConverterSequenceTypeFfiConsent.lower(`consent`), _status + ) + } + } + + + override fun `onError`(`error`: FfiSubscribeException) = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_fficonsentcallback_on_error( + it, FfiConverterTypeFfiSubscribeError.lower(`error`), _status + ) + } + } + + + companion object + +} + +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +internal const val IDX_CALLBACK_FREE = 0 + +// Callback return codes +internal const val UNIFFI_CALLBACK_SUCCESS = 0 +internal const val UNIFFI_CALLBACK_ERROR = 1 +internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 + +/** + * @suppress + */ +public abstract class FfiConverterCallbackInterface : + FfiConverter { + internal val handleMap = UniffiHandleMap() + + internal fun drop(handle: Long) { + handleMap.remove(handle) + } + + override fun lift(value: Long): CallbackInterface { + return handleMap.get(value) + } + + override fun read(buf: ByteBuffer) = lift(buf.getLong()) + + override fun lower(value: CallbackInterface) = handleMap.insert(value) + + override fun allocationSize(value: CallbackInterface) = 8UL + + override fun write(value: CallbackInterface, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} + +// Put the implementation in an object so we don't pollute the top-level namespace +internal object uniffiCallbackInterfaceFfiConsentCallback { + internal object `onConsentUpdate` : UniffiCallbackInterfaceFfiConsentCallbackMethod0 { + override fun callback( + `uniffiHandle`: Long, + `consent`: RustBuffer.ByValue, + `uniffiOutReturn`: Pointer, + uniffiCallStatus: UniffiRustCallStatus, + ) { + val uniffiObj = FfiConverterTypeFfiConsentCallback.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`onConsentUpdate`( + FfiConverterSequenceTypeFfiConsent.lift(`consent`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) + } + } + + internal object `onError` : UniffiCallbackInterfaceFfiConsentCallbackMethod1 { + override fun callback( + `uniffiHandle`: Long, + `error`: RustBuffer.ByValue, + `uniffiOutReturn`: Pointer, + uniffiCallStatus: UniffiRustCallStatus, + ) { + val uniffiObj = FfiConverterTypeFfiConsentCallback.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`onError`( + FfiConverterTypeFfiSubscribeError.lift(`error`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) + } + } + + internal object uniffiFree : UniffiCallbackInterfaceFree { + override fun callback(handle: Long) { + FfiConverterTypeFfiConsentCallback.handleMap.remove(handle) + } + } + + internal var vtable = UniffiVTableCallbackInterfaceFfiConsentCallback.UniffiByValue( + `onConsentUpdate`, + `onError`, + uniffiFree, + ) + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal fun register(lib: UniffiLib) { + lib.uniffi_xmtpv3_fn_init_callback_vtable_fficonsentcallback(vtable) + } +} + +/** + * @suppress + */ +public object FfiConverterTypeFfiConsentCallback : FfiConverter { + internal val handleMap = UniffiHandleMap() + + override fun lower(value: FfiConsentCallback): Pointer { + return Pointer(handleMap.insert(value)) + } + + override fun lift(value: Pointer): FfiConsentCallback { + return FfiConsentCallbackImpl(value) + } + + override fun read(buf: ByteBuffer): FfiConsentCallback { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: FfiConsentCallback) = 8UL + + override fun write(value: FfiConsentCallback, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + public interface FfiConversationInterface { suspend fun `addAdmin`(`inboxId`: kotlin.String) @@ -4310,40 +4766,6 @@ open class FfiConversationCallbackImpl : Disposable, AutoCloseable, FfiConversat } -// Magic number for the Rust proxy to call using the same mechanism as every other method, -// to free the callback once it's dropped by Rust. -internal const val IDX_CALLBACK_FREE = 0 - -// Callback return codes -internal const val UNIFFI_CALLBACK_SUCCESS = 0 -internal const val UNIFFI_CALLBACK_ERROR = 1 -internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 - -/** - * @suppress - */ -public abstract class FfiConverterCallbackInterface : - FfiConverter { - internal val handleMap = UniffiHandleMap() - - internal fun drop(handle: Long) { - handleMap.remove(handle) - } - - override fun lift(value: Long): CallbackInterface { - return handleMap.get(value) - } - - override fun read(buf: ByteBuffer) = lift(buf.getLong()) - - override fun lower(value: CallbackInterface) = handleMap.insert(value) - - override fun allocationSize(value: CallbackInterface) = 8UL - - override fun write(value: CallbackInterface, buf: ByteBuffer) { - buf.putLong(lower(value)) - } -} // Put the implementation in an object so we don't pollute the top-level namespace internal object uniffiCallbackInterfaceFfiConversationCallback { @@ -4788,6 +5210,8 @@ public interface FfiConversationsInterface { `opts`: FfiCreateGroupOptions, ): FfiConversation + fun `getSyncGroup`(): FfiConversation + suspend fun `list`(`opts`: FfiListConversationsOptions): List suspend fun `listDms`(`opts`: FfiListConversationsOptions): List @@ -4804,6 +5228,8 @@ public interface FfiConversationsInterface { suspend fun `streamAllMessages`(`messageCallback`: FfiMessageCallback): FfiStreamCloser + suspend fun `streamConsent`(`callback`: FfiConsentCallback): FfiStreamCloser + suspend fun `streamDms`(`callback`: FfiConversationCallback): FfiStreamCloser suspend fun `streamGroups`(`callback`: FfiConversationCallback): FfiStreamCloser @@ -4965,6 +5391,20 @@ open class FfiConversations : Disposable, AutoCloseable, FfiConversationsInterfa } + @Throws(GenericException::class) + override fun `getSyncGroup`(): FfiConversation { + return FfiConverterTypeFfiConversation.lift( + callWithPointer { + uniffiRustCallWithError(GenericException) { _status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_fficonversations_get_sync_group( + it, _status + ) + } + } + ) + } + + @Throws(GenericException::class) @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") override suspend fun `list`(`opts`: FfiListConversationsOptions): List { @@ -5217,6 +5657,37 @@ open class FfiConversations : Disposable, AutoCloseable, FfiConversationsInterfa } + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + override suspend fun `streamConsent`(`callback`: FfiConsentCallback): FfiStreamCloser { + return uniffiRustCallAsync( + callWithPointer { thisPtr -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_fficonversations_stream_consent( + thisPtr, + FfiConverterTypeFfiConsentCallback.lower(`callback`), + ) + }, + { future, callback, continuation -> + UniffiLib.INSTANCE.ffi_xmtpv3_rust_future_poll_pointer( + future, + callback, + continuation + ) + }, + { future, continuation -> + UniffiLib.INSTANCE.ffi_xmtpv3_rust_future_complete_pointer( + future, + continuation + ) + }, + { future -> UniffiLib.INSTANCE.ffi_xmtpv3_rust_future_free_pointer(future) }, + // lift function + { FfiConverterTypeFfiStreamCloser.lift(it) }, + // Error FFI converter + UniffiNullRustCallStatusErrorHandler, + ) + } + + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") override suspend fun `streamDms`(`callback`: FfiConversationCallback): FfiStreamCloser { return uniffiRustCallAsync( @@ -7849,6 +8320,17 @@ public interface FfiXmtpClientInterface { fun `signatureRequest`(): FfiSignatureRequest? + fun `verifySignedWithInstallationKey`( + `signatureText`: kotlin.String, + `signatureBytes`: kotlin.ByteArray, + ) + + fun `verifySignedWithPublicKey`( + `signatureText`: kotlin.String, + `signatureBytes`: kotlin.ByteArray, + `publicKey`: kotlin.ByteArray, + ) + companion object } @@ -8573,6 +9055,42 @@ open class FfiXmtpClient : Disposable, AutoCloseable, FfiXmtpClientInterface { } + @Throws(GenericException::class) + override fun `verifySignedWithInstallationKey`( + `signatureText`: kotlin.String, + `signatureBytes`: kotlin.ByteArray, + ) = + callWithPointer { + uniffiRustCallWithError(GenericException) { _status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_ffixmtpclient_verify_signed_with_installation_key( + it, + FfiConverterString.lower(`signatureText`), + FfiConverterByteArray.lower(`signatureBytes`), + _status + ) + } + } + + + @Throws(GenericException::class) + override fun `verifySignedWithPublicKey`( + `signatureText`: kotlin.String, + `signatureBytes`: kotlin.ByteArray, + `publicKey`: kotlin.ByteArray, + ) = + callWithPointer { + uniffiRustCallWithError(GenericException) { _status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_ffixmtpclient_verify_signed_with_public_key( + it, + FfiConverterString.lower(`signatureText`), + FfiConverterByteArray.lower(`signatureBytes`), + FfiConverterByteArray.lower(`publicKey`), + _status + ) + } + } + + companion object } diff --git a/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so b/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so index 13c0a974b..6297ead10 100644 Binary files a/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so differ diff --git a/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so b/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so index 8745d3a2a..d69fd01f4 100644 Binary files a/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so differ diff --git a/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so b/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so index bd32c1caa..363585581 100644 Binary files a/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so differ diff --git a/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so b/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so index 5826c23b9..8ac54101c 100644 Binary files a/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so differ