diff --git a/bindings_ffi/examples/MainActivity.kt b/bindings_ffi/examples/MainActivity.kt index 4f981ab47..fd015c913 100644 --- a/bindings_ffi/examples/MainActivity.kt +++ b/bindings_ffi/examples/MainActivity.kt @@ -5,19 +5,24 @@ import android.util.Log import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.example.xmtpv3_example.R.id.selftest_output +import java.io.File +import java.nio.charset.StandardCharsets +import java.security.SecureRandom import kotlinx.coroutines.runBlocking import org.bouncycastle.util.encoders.Hex.toHexString import org.web3j.crypto.Credentials import org.web3j.crypto.ECKeyPair import org.web3j.crypto.Sign +import org.xmtp.android.library.Client +import org.xmtp.android.library.ClientOptions +import org.xmtp.android.library.XMTPEnvironment +import org.xmtp.android.library.messages.PrivateKeyBuilder +import org.xmtp.android.library.messages.toV2 import uniffi.xmtpv3.FfiConversationCallback import uniffi.xmtpv3.FfiGroup import uniffi.xmtpv3.FfiInboxOwner import uniffi.xmtpv3.FfiLogger import uniffi.xmtpv3.LegacyIdentitySource -import java.io.File -import java.nio.charset.StandardCharsets -import java.security.SecureRandom const val EMULATOR_LOCALHOST_ADDRESS = "http://10.0.2.2:5556" const val DEV_NETWORK_ADDRESS = "https://dev.xmtp.network:5556" @@ -40,9 +45,15 @@ class AndroidFfiLogger : FfiLogger { } } -class ConversationCallback: FfiConversationCallback { +class ConversationCallback : FfiConversationCallback { override fun onConversation(conversation: FfiGroup) { - Log.i("App", "INFO - Conversation callback with ID: " + toHexString(conversation.id()) + ", members: " + conversation.listMembers()) + Log.i( + "App", + "INFO - Conversation callback with ID: " + + toHexString(conversation.id()) + + ", members: " + + conversation.listMembers() + ) } } @@ -54,44 +65,61 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) val textView: TextView = findViewById(selftest_output) - val privateKey: ByteArray = SecureRandom().generateSeed(32) - val credentials: Credentials = Credentials.create(ECKeyPair.create(privateKey)) - val inboxOwner = Web3jInboxOwner(credentials) val dbDir: File = File(this.filesDir.absolutePath, "xmtp_db") + try { + dbDir.deleteRecursively() + } catch (e: Exception) {} dbDir.mkdir() val dbPath: String = dbDir.absolutePath + "/android_example.db3" val dbEncryptionKey = SecureRandom().generateSeed(32) Log.i( - "App", - "INFO -\naccountAddress: " + inboxOwner.getAddress() + "\nprivateKey: " + privateKey.asList() + "\nDB path: " + dbPath + "\nDB encryption key: " + dbEncryptionKey + "App", + "INFO -\nDB path: " + + dbPath + + "\nDB encryption key: " + + dbEncryptionKey ) runBlocking { try { - val client = uniffi.xmtpv3.createClient( - AndroidFfiLogger(), - EMULATOR_LOCALHOST_ADDRESS, - false, - dbPath, - dbEncryptionKey, - inboxOwner.getAddress(), - LegacyIdentitySource.NONE, - null, - ) - var walletSignature: ByteArray? = null; - val textToSign = client.textToSign(); + val key = PrivateKeyBuilder() + val client = + uniffi.xmtpv3.createClient( + AndroidFfiLogger(), + EMULATOR_LOCALHOST_ADDRESS, + false, + dbPath, + dbEncryptionKey, + key.address, + LegacyIdentitySource.KEY_GENERATOR, + getV2SerializedSignedPrivateKey(key), + ) + var walletSignature: ByteArray? = null + val textToSign = client.textToSign() if (textToSign != null) { - walletSignature = inboxOwner.sign(textToSign) + walletSignature = key.sign(textToSign).toByteArray() } client.registerIdentity(walletSignature); textView.text = "Libxmtp version\n" + uniffi.xmtpv3.getVersionInfo() + "\n\nClient constructed, wallet address: " + client.accountAddress() Log.i("App", "Setting up conversation streaming") - client.conversations().stream(ConversationCallback()); + client.conversations().stream(ConversationCallback()) } catch (e: Exception) { textView.text = "Failed to construct client: " + e.message } } + } - dbDir.deleteRecursively() + fun getV2SerializedSignedPrivateKey(key: PrivateKeyBuilder): ByteArray { + val options = + ClientOptions( + api = + ClientOptions.Api( + env = XMTPEnvironment.LOCAL, + isSecure = false, + ), + appContext = this@MainActivity + ) + val client = Client().create(account = key, options = options) + return client.privateKeyBundleV1.toV2().identityKey.toByteArray(); } } diff --git a/bindings_ffi/jniLibs/arm64-v8a/libuniffi_xmtpv3.so b/bindings_ffi/jniLibs/arm64-v8a/libuniffi_xmtpv3.so index ab68b6d37..e88f45c2c 100755 Binary files a/bindings_ffi/jniLibs/arm64-v8a/libuniffi_xmtpv3.so and b/bindings_ffi/jniLibs/arm64-v8a/libuniffi_xmtpv3.so differ diff --git a/bindings_ffi/src/logger.rs b/bindings_ffi/src/logger.rs index 44ce84d46..2c4b987d9 100644 --- a/bindings_ffi/src/logger.rs +++ b/bindings_ffi/src/logger.rs @@ -20,7 +20,7 @@ impl log::Log for RustLogger { self.logger.lock().expect("Logger mutex is poisoned!").log( record.level() as u32, record.level().to_string(), - record.args().to_string(), + format!("[libxmtp] {}", record.args().to_string()), ); } } diff --git a/bindings_ffi/src/mls.rs b/bindings_ffi/src/mls.rs index 4a2246053..09fdf5053 100644 --- a/bindings_ffi/src/mls.rs +++ b/bindings_ffi/src/mls.rs @@ -616,6 +616,41 @@ mod tests { assert!(!client.account_address().is_empty()); } + #[tokio::test] + async fn test_legacy_identity() { + let legacy_address = "0x419cb1fa5635b0c6df47c9dc5765c8f1f4dff78e"; + let legacy_signed_private_key_proto = vec![ + 8, 128, 154, 196, 133, 220, 244, 197, 216, 23, 18, 34, 10, 32, 214, 70, 104, 202, 68, + 204, 25, 202, 197, 141, 239, 159, 145, 249, 55, 242, 147, 126, 3, 124, 159, 207, 96, + 135, 134, 122, 60, 90, 82, 171, 131, 162, 26, 153, 1, 10, 79, 8, 128, 154, 196, 133, + 220, 244, 197, 216, 23, 26, 67, 10, 65, 4, 232, 32, 50, 73, 113, 99, 115, 168, 104, + 229, 206, 24, 217, 132, 223, 217, 91, 63, 137, 136, 50, 89, 82, 186, 179, 150, 7, 127, + 140, 10, 165, 117, 233, 117, 196, 134, 227, 143, 125, 210, 187, 77, 195, 169, 162, 116, + 34, 20, 196, 145, 40, 164, 246, 139, 197, 154, 233, 190, 148, 35, 131, 240, 106, 103, + 18, 70, 18, 68, 10, 64, 90, 24, 36, 99, 130, 246, 134, 57, 60, 34, 142, 165, 221, 123, + 63, 27, 138, 242, 195, 175, 212, 146, 181, 152, 89, 48, 8, 70, 104, 94, 163, 0, 25, + 196, 228, 190, 49, 108, 141, 60, 174, 150, 177, 115, 229, 138, 92, 105, 170, 226, 204, + 249, 206, 12, 37, 145, 3, 35, 226, 15, 49, 20, 102, 60, 16, 1, + ]; + + let client = create_client( + Box::new(MockLogger {}), + xmtp_api_grpc::LOCALHOST_ADDRESS.to_string(), + false, + Some(tmp_path()), + None, + legacy_address.to_string(), + LegacyIdentitySource::KeyGenerator, + Some(legacy_signed_private_key_proto), + ) + .await + .unwrap(); + + assert!(client.text_to_sign().is_none()); + client.register_identity(None).await.unwrap(); + assert_eq!(client.account_address(), legacy_address); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_create_client_with_storage() { let ffi_inbox_owner = LocalWalletInboxOwner::new(); diff --git a/examples/android/xmtpv3_example/app/build.gradle b/examples/android/xmtpv3_example/app/build.gradle index b28b465c3..66b820f55 100644 --- a/examples/android/xmtpv3_example/app/build.gradle +++ b/examples/android/xmtpv3_example/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.example.xmtpv3_example" - minSdk 21 + minSdk 23 targetSdk 33 versionCode 1 versionName "1.0" @@ -37,6 +37,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.core:core-ktx:1.7.0' implementation 'com.google.android.material:material:1.8.0' + implementation "org.xmtp:android:0.7.10" implementation "net.java.dev.jna:jna:5.13.0@aar" implementation 'org.web3j:crypto:5.0.0' testImplementation 'junit:junit:4.13.2' diff --git a/examples/android/xmtpv3_example/build.gradle b/examples/android/xmtpv3_example/build.gradle index 253697423..27f9898ce 100644 --- a/examples/android/xmtpv3_example/build.gradle +++ b/examples/android/xmtpv3_example/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.3.1' apply false - id 'com.android.library' version '7.3.1' apply false + id 'com.android.application' version '8.0.0-rc01' apply false + id 'com.android.library' version '8.0.0-rc01' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false } \ No newline at end of file diff --git a/examples/android/xmtpv3_example/gradle.properties b/examples/android/xmtpv3_example/gradle.properties index 3c5031eb7..a2e90d87b 100644 --- a/examples/android/xmtpv3_example/gradle.properties +++ b/examples/android/xmtpv3_example/gradle.properties @@ -20,4 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/examples/android/xmtpv3_example/gradle/wrapper/gradle-wrapper.properties b/examples/android/xmtpv3_example/gradle/wrapper/gradle-wrapper.properties index b56820a0b..7a506b50d 100644 --- a/examples/android/xmtpv3_example/gradle/wrapper/gradle-wrapper.properties +++ b/examples/android/xmtpv3_example/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Mar 29 17:13:20 PDT 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/xmtp_mls/src/builder.rs b/xmtp_mls/src/builder.rs index 25736b54a..f30263394 100644 --- a/xmtp_mls/src/builder.rs +++ b/xmtp_mls/src/builder.rs @@ -3,6 +3,7 @@ use std::println as debug; #[cfg(not(test))] use log::debug; +use log::info; use thiserror::Error; use xmtp_proto::api_client::XmtpMlsClient; @@ -38,7 +39,7 @@ pub enum ClientBuilderError { // AssociationFailed(#[from] AssociationError), // #[error("Error Initializing Store")] // StoreInitialization(#[from] SE), - #[error("Error Initalizing Identity")] + #[error("Error initializing identity: {0}")] IdentityInitialization(#[from] IdentityError), #[error("Storage Error")] @@ -82,6 +83,7 @@ impl IdentityStrategy { api_client: &ApiClientWrapper, store: &EncryptedMessageStore, ) -> Result { + info!("Initializing identity"); let conn = store.conn()?; let provider = XmtpOpenMlsProvider::new(&conn); let identity_option: Option = provider @@ -117,6 +119,7 @@ impl IdentityStrategy { account_address: String, legacy_identity: LegacyIdentity, ) -> Result { + info!("Creating identity"); let identity = match legacy_identity { // This is a fresh install, and at most one v2 signature (enable_identity) // has been requested so far, so it's fine to request another one (grant_messaging_access). diff --git a/xmtp_mls/src/credential/legacy_create_identity_association.rs b/xmtp_mls/src/credential/legacy_create_identity_association.rs index b168b2d37..16f3c4dd2 100644 --- a/xmtp_mls/src/credential/legacy_create_identity_association.rs +++ b/xmtp_mls/src/credential/legacy_create_identity_association.rs @@ -42,7 +42,9 @@ impl LegacyCreateIdentityAssociation { LegacySignedPrivateKeyProto::decode(legacy_signed_private_key.as_slice())?; let signed_private_key::Union::Secp256k1(secp256k1) = legacy_signed_private_key_proto .union - .ok_or(AssociationError::MalformedLegacyKey)?; + .ok_or(AssociationError::MalformedLegacyKey( + "Missing secp256k1.union field".to_string(), + ))?; let legacy_private_key = secp256k1.bytes; let (mut delegating_signature, recovery_id) = k256_helper::sign_sha256( &legacy_private_key, // secret_key @@ -51,9 +53,9 @@ impl LegacyCreateIdentityAssociation { .map_err(AssociationError::LegacySignature)?; delegating_signature.push(recovery_id); // TODO: normalize recovery ID if necessary - let legacy_signed_public_key_proto = legacy_signed_private_key_proto - .public_key - .ok_or(AssociationError::MalformedLegacyKey)?; + let legacy_signed_public_key_proto = legacy_signed_private_key_proto.public_key.ok_or( + AssociationError::MalformedLegacyKey("Missing public_key field".to_string()), + )?; Self::new_validated( installation_public_key, delegating_signature, diff --git a/xmtp_mls/src/credential/mod.rs b/xmtp_mls/src/credential/mod.rs index e6a415875..f28bbc335 100644 --- a/xmtp_mls/src/credential/mod.rs +++ b/xmtp_mls/src/credential/mod.rs @@ -25,10 +25,10 @@ use self::legacy_create_identity_association::LegacyCreateIdentityAssociation; pub enum AssociationError { #[error("bad signature")] BadSignature(#[from] SignatureError), - #[error("decode error")] + #[error("decode error: {0}")] DecodeError(#[from] DecodeError), - #[error("legacy key")] - MalformedLegacyKey, + #[error("legacy key: {0}")] + MalformedLegacyKey(String), #[error("legacy signature: {0}")] LegacySignature(String), #[error("Association text mismatch")] diff --git a/xmtp_mls/src/credential/validated_legacy_signed_public_key.rs b/xmtp_mls/src/credential/validated_legacy_signed_public_key.rs index 775523f6a..ad132c841 100644 --- a/xmtp_mls/src/credential/validated_legacy_signed_public_key.rs +++ b/xmtp_mls/src/credential/validated_legacy_signed_public_key.rs @@ -1,3 +1,4 @@ +use log::info; use prost::Message; use xmtp_cryptography::signature::RecoverableSignature; @@ -61,19 +62,35 @@ impl TryFrom for ValidatedLegacySignedPublicKey { fn try_from(proto: LegacySignedPublicKeyProto) -> Result { let serialized_key_data = proto.key_bytes; - let Union::WalletEcdsaCompact(wallet_ecdsa_compact) = proto + let union = proto .signature - .ok_or(AssociationError::MalformedLegacyKey)? + .ok_or(AssociationError::MalformedLegacyKey( + "Missing signature field".to_string(), + ))? .union - .ok_or(AssociationError::MalformedLegacyKey)? - else { - return Err(AssociationError::MalformedLegacyKey); + .ok_or(AssociationError::MalformedLegacyKey( + "Missing signature.union field".to_string(), + ))?; + let wallet_signature = match union { + Union::WalletEcdsaCompact(wallet_ecdsa_compact) => { + info!("Reading WalletEcdsaCompact from legacy key"); + let mut wallet_signature = wallet_ecdsa_compact.bytes.clone(); + wallet_signature.push(wallet_ecdsa_compact.recovery as u8); // TODO: normalize recovery ID if necessary + if wallet_signature.len() != 65 { + return Err(AssociationError::MalformedAssociation); + } + wallet_signature + } + Union::EcdsaCompact(ecdsa_compact) => { + info!("Reading EcdsaCompact from legacy key"); + let mut signature = ecdsa_compact.bytes.clone(); + signature.push(ecdsa_compact.recovery as u8); // TODO: normalize recovery ID if necessary + if signature.len() != 65 { + return Err(AssociationError::MalformedAssociation); + } + signature + } }; - let mut wallet_signature = wallet_ecdsa_compact.bytes.clone(); - wallet_signature.push(wallet_ecdsa_compact.recovery as u8); // TODO: normalize recovery ID if necessary - if wallet_signature.len() != 65 { - return Err(AssociationError::MalformedAssociation); - } let wallet_signature = RecoverableSignature::Eip191Signature(wallet_signature); let account_address = wallet_signature.recover_address(&Self::text(&serialized_key_data))?; diff --git a/xmtp_mls/src/identity.rs b/xmtp_mls/src/identity.rs index 97d35105a..5098ec33b 100644 --- a/xmtp_mls/src/identity.rs +++ b/xmtp_mls/src/identity.rs @@ -1,5 +1,6 @@ use std::sync::RwLock; +use log::info; use openmls::{ credentials::errors::CredentialError, extensions::{errors::InvalidExtensionError, ApplicationIdExtension, LastResortExtension}, @@ -33,19 +34,19 @@ use crate::{ #[derive(Debug, Error)] pub enum IdentityError { - #[error("generating new identity")] + #[error("generating new identity: {0}")] BadGeneration(#[from] SignatureError), - #[error("bad association")] + #[error("bad association: {0}")] BadAssocation(#[from] AssociationError), - #[error("generating key-pairs")] + #[error("generating key-pairs: {0}")] KeyGenerationError(#[from] CryptoError), - #[error("storage error")] + #[error("storage error: {0}")] StorageError(#[from] StorageError), - #[error("generating key package")] + #[error("generating key package: {0}")] KeyPackageGenerationError(#[from] KeyPackageNewError), - #[error("deserialization")] + #[error("deserialization: {0}")] Deserialization(#[from] prost::DecodeError), - #[error("invalid extension")] + #[error("invalid extension: {0}")] InvalidExtension(#[from] InvalidExtensionError), #[error("uninitialized identity")] UninitializedIdentity, @@ -92,12 +93,14 @@ impl Identity { account_address: String, legacy_signed_private_key: Vec, ) -> Result { + info!("Creating identity from legacy key"); let signature_keys = SignatureKeyPair::new(CIPHERSUITE.signature_algorithm()).unwrap(); let credential = Credential::create_from_legacy(&signature_keys, legacy_signed_private_key)?; let credential_proto: CredentialProto = credential.into(); let mls_credential = OpenMlsCredential::new(credential_proto.encode_to_vec(), CredentialType::Basic)?; + info!("Successfully created identity from legacy key"); Ok(Self { account_address, installation_keys: signature_keys, @@ -115,9 +118,11 @@ impl Identity { // Do not re-register if already registered let stored_identity: Option = provider.conn().fetch(&())?; if stored_identity.is_some() { + info!("Identity already registered, skipping registration"); return Ok(()); } + info!("Registering identity"); // If we do not have a signed credential, apply the provided signature if self.credential().is_err() { if recoverable_wallet_signature.is_none() {