diff --git a/Cargo.lock b/Cargo.lock index 93f528a86..085faa1ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6192,9 +6192,6 @@ dependencies = [ "base64 0.22.1", "futures", "hex", - "log", - "pbjson", - "pbjson-types", "prost", "serde", "tokio", @@ -6217,8 +6214,6 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-stream", - "tracing", "xmtp_proto", ] @@ -6229,7 +6224,6 @@ dependencies = [ "clap", "ethers", "femme", - "futures", "hex", "kv-log-macro", "log", @@ -6239,8 +6233,6 @@ dependencies = [ "thiserror", "timeago", "tokio", - "tokio-stream", - "url", "xmtp_api_grpc", "xmtp_cryptography", "xmtp_id", @@ -6276,15 +6268,11 @@ dependencies = [ "async-trait", "chrono", "ctor", - "ed25519", "ed25519-dalek", "ethers", "futures", "hex", "log", - "openmls", - "openmls_basic_credential", - "openmls_rust_crypto", "openmls_traits", "prost", "rand", @@ -6294,7 +6282,6 @@ dependencies = [ "sha2 0.10.8", "thiserror", "tokio", - "tracing", "url", "xmtp_cryptography", "xmtp_proto", @@ -6310,7 +6297,6 @@ dependencies = [ "async-barrier", "async-stream", "bincode", - "chrono", "criterion", "ctor", "diesel", @@ -6353,7 +6339,6 @@ dependencies = [ "xmtp_cryptography", "xmtp_id", "xmtp_proto", - "xmtp_v2", ] [[package]] @@ -6361,9 +6346,7 @@ name = "xmtp_proto" version = "0.0.1" dependencies = [ "futures", - "futures-core", "openmls", - "openmls_basic_credential", "pbjson", "pbjson-types", "prost", @@ -6377,9 +6360,8 @@ dependencies = [ name = "xmtp_user_preferences" version = "0.0.1" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "libsecp256k1", - "once_cell", "prost", "rand", "xmtp_proto", diff --git a/bindings_ffi/Cargo.lock b/bindings_ffi/Cargo.lock index b78ae1a68..e01293e1e 100644 --- a/bindings_ffi/Cargo.lock +++ b/bindings_ffi/Cargo.lock @@ -1238,29 +1238,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "env_filter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -2150,12 +2127,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.28" @@ -2595,15 +2566,6 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "matchit" version = "0.7.3" @@ -3650,19 +3612,10 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata", "regex-syntax 0.8.2", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - [[package]] name = "regex-automata" version = "0.4.4" @@ -3674,12 +3627,6 @@ dependencies = [ "regex-syntax 0.8.2", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.7.5" @@ -4616,9 +4563,9 @@ dependencies = [ [[package]] name = "thread-id" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" dependencies = [ "libc", "winapi", @@ -5017,14 +4964,10 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] @@ -5753,9 +5696,6 @@ dependencies = [ "base64 0.22.1", "futures", "hex", - "log", - "pbjson", - "pbjson-types", "prost", "serde", "tokio", @@ -5791,15 +5731,11 @@ version = "0.0.1" dependencies = [ "async-trait", "chrono", - "ed25519", "ed25519-dalek", "ethers", "futures", "hex", "log", - "openmls", - "openmls_basic_credential", - "openmls_rust_crypto", "openmls_traits", "prost", "rand", @@ -5809,7 +5745,6 @@ dependencies = [ "sha2", "thiserror", "tokio", - "tracing", "url", "xmtp_cryptography", "xmtp_proto", @@ -5822,7 +5757,6 @@ dependencies = [ "aes-gcm", "async-stream", "bincode", - "chrono", "diesel", "diesel_migrations", "ed25519-dalek", @@ -5848,10 +5782,10 @@ dependencies = [ "toml 0.8.8", "tracing", "trait-variant", + "xmtp_api_grpc", "xmtp_cryptography", "xmtp_id", "xmtp_proto", - "xmtp_v2", ] [[package]] @@ -5859,9 +5793,7 @@ name = "xmtp_proto" version = "0.0.1" dependencies = [ "futures", - "futures-core", "openmls", - "openmls_basic_credential", "pbjson", "pbjson-types", "prost", @@ -5875,8 +5807,7 @@ dependencies = [ name = "xmtp_user_preferences" version = "0.0.1" dependencies = [ - "base64 0.21.7", - "once_cell", + "base64 0.22.1", "prost", "xmtp_proto", "xmtp_v2", @@ -5903,7 +5834,6 @@ dependencies = [ name = "xmtpv3" version = "0.0.1" dependencies = [ - "env_logger", "ethers", "futures", "log", @@ -5916,7 +5846,6 @@ dependencies = [ "tokio-test", "tracing-subscriber", "uniffi", - "uniffi_macros", "uuid 1.9.1", "xmtp_api_grpc", "xmtp_cryptography", diff --git a/bindings_ffi/Cargo.toml b/bindings_ffi/Cargo.toml index 515a01db0..5549c957f 100644 --- a/bindings_ffi/Cargo.toml +++ b/bindings_ffi/Cargo.toml @@ -7,7 +7,6 @@ version = "0.0.1" crate-type = ["lib", "cdylib", "staticlib"] [dependencies] -env_logger = "0.11.3" futures = "0.3.28" log = { version = "0.4", features = ["std"] } parking_lot = "0.12.3" @@ -15,7 +14,6 @@ thiserror = "1.0" thread-id = "4.2.1" tokio = { version = "1.28.1", features = ["macros"] } uniffi = { version = "0.28.0", features = ["tokio", "cli"] } -uniffi_macros = "0.28.0" xmtp_api_grpc = { path = "../xmtp_api_grpc" } xmtp_cryptography = { path = "../xmtp_cryptography" } xmtp_id = { path = "../xmtp_id" } @@ -24,8 +22,7 @@ xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] } xmtp_user_preferences = { path = "../xmtp_user_preferences" } xmtp_v2 = { path = "../xmtp_v2" } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -# NOTE: A regression in openssl-sys exists where libatomic is dynamically linked +# NOTE: A regression in openssl-sys exists where libatomic is dynamically linked # for i686-linux-android targets. https://github.com/sfackler/rust-openssl/issues/2163 # # This is fixed in the openssl-sys fork at @@ -50,6 +47,7 @@ tokio-test = "0.4" tracing-subscriber = "0.3" uniffi = { version = "0.28.0", features = ["bindgen-tests"] } uuid = { version = "1.9", features = ["v4", "fast-rng"] } +xmtp_mls = { path = "../xmtp_mls", features = ["native", "test-utils"] } # NOTE: The release profile reduces bundle size from 230M to 41M - may have performance impliciations # https://stackoverflow.com/a/54842093 diff --git a/bindings_ffi/src/mls.rs b/bindings_ffi/src/mls.rs index 32976a29e..1d610ff6c 100644 --- a/bindings_ffi/src/mls.rs +++ b/bindings_ffi/src/mls.rs @@ -1720,7 +1720,7 @@ mod tests { xmtp_api_grpc::LOCALHOST_ADDRESS.to_string(), false, Some(tmp_path()), - None, + Some(xmtp_mls::storage::EncryptedMessageStore::generate_enc_key().into()), &inbox_id, ffi_inbox_owner.get_address(), nonce, diff --git a/bindings_node/Cargo.lock b/bindings_node/Cargo.lock index acfd0213f..03a2852a2 100644 --- a/bindings_node/Cargo.lock +++ b/bindings_node/Cargo.lock @@ -5201,9 +5201,6 @@ dependencies = [ "base64 0.22.1", "futures", "hex", - "log", - "pbjson", - "pbjson-types", "prost", "serde", "tokio", @@ -5239,15 +5236,11 @@ version = "0.0.1" dependencies = [ "async-trait", "chrono", - "ed25519", "ed25519-dalek", "ethers", "futures", "hex", "log", - "openmls", - "openmls_basic_credential", - "openmls_rust_crypto", "openmls_traits", "prost", "rand", @@ -5257,7 +5250,6 @@ dependencies = [ "sha2", "thiserror", "tokio", - "tracing", "url", "xmtp_cryptography", "xmtp_proto", @@ -5270,7 +5262,6 @@ dependencies = [ "aes-gcm", "async-stream", "bincode", - "chrono", "diesel", "diesel_migrations", "ed25519-dalek", @@ -5296,10 +5287,10 @@ dependencies = [ "toml", "tracing", "trait-variant", + "xmtp_api_grpc", "xmtp_cryptography", "xmtp_id", "xmtp_proto", - "xmtp_v2", ] [[package]] @@ -5307,9 +5298,7 @@ name = "xmtp_proto" version = "0.0.1" dependencies = [ "futures", - "futures-core", "openmls", - "openmls_basic_credential", "pbjson", "pbjson-types", "prost", diff --git a/examples/cli/Cargo.toml b/examples/cli/Cargo.toml index 3f8329805..f85bcc0b0 100644 --- a/examples/cli/Cargo.toml +++ b/examples/cli/Cargo.toml @@ -16,7 +16,6 @@ path = "cli-client.rs" clap = { version = "4.4.6", features = ["derive"] } ethers = "2.0.4" femme = "2.2.1" -futures = "0.3.28" hex = "0.4.3" kv-log-macro = "1.0.7" log = { workspace = true, features = [ @@ -30,8 +29,6 @@ serde_json.workspace = true thiserror.workspace = true timeago = "0.4.1" tokio = "1.28.1" -tokio-stream = "0.1.15" -url = "2.3.1" xmtp_api_grpc = { path = "../../xmtp_api_grpc" } xmtp_cryptography = { path = "../../xmtp_cryptography" } xmtp_id = { path = "../../xmtp_id" } diff --git a/xmtp_api_grpc/Cargo.toml b/xmtp_api_grpc/Cargo.toml index d96a19b1d..f0d6b8bac 100644 --- a/xmtp_api_grpc/Cargo.toml +++ b/xmtp_api_grpc/Cargo.toml @@ -8,9 +8,6 @@ async-stream.workspace = true base64 = "0.22" futures.workspace = true hex.workspace = true -log = { workspace = true, features = ["std"] } -pbjson-types.workspace = true -pbjson.workspace = true prost = { workspace = true, features = ["prost-derive"] } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } diff --git a/xmtp_api_http/Cargo.toml b/xmtp_api_http/Cargo.toml index 2a2b9cb8d..385ff7098 100644 --- a/xmtp_api_http/Cargo.toml +++ b/xmtp_api_http/Cargo.toml @@ -15,8 +15,6 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = "1.0" tokio = { workspace = true, features = ["sync", "rt", "macros"] } -tokio-stream = { version = "0.1", default-features = false } -tracing.workspace = true xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] } [dev-dependencies] diff --git a/xmtp_id/Cargo.toml b/xmtp_id/Cargo.toml index 94f82f343..37628185b 100644 --- a/xmtp_id/Cargo.toml +++ b/xmtp_id/Cargo.toml @@ -7,14 +7,10 @@ version.workspace = true async-trait.workspace = true chrono.workspace = true ed25519-dalek = { workspace = true, features = ["digest"] } -ed25519.workspace = true ethers.workspace = true futures.workspace = true hex.workspace = true log.workspace = true -openmls.workspace = true -openmls_basic_credential.workspace = true -openmls_rust_crypto.workspace = true openmls_traits.workspace = true prost.workspace = true rand.workspace = true @@ -24,7 +20,6 @@ serde.workspace = true sha2.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["macros"] } -tracing.workspace = true url.workspace = true xmtp_cryptography.workspace = true xmtp_proto = { workspace = true, features = ["proto_full"] } diff --git a/xmtp_mls/Cargo.toml b/xmtp_mls/Cargo.toml index 91ab0ded0..7e41a8052 100644 --- a/xmtp_mls/Cargo.toml +++ b/xmtp_mls/Cargo.toml @@ -20,15 +20,14 @@ bench = [ ] default = ["native"] http-api = ["xmtp_api_http"] -native = ["libsqlite3-sys/bundled-sqlcipher-vendored-openssl"] -test-utils = [] +native = ["libsqlite3-sys/bundled-sqlcipher-vendored-openssl", "xmtp_api_grpc"] +test-utils = ["xmtp_id/test-utils"] [dependencies] aes-gcm = { version = "0.10.3", features = ["std"] } async-stream.workspace = true trait-variant.workspace = true bincode = "1.3.3" -chrono = { workspace = true } diesel = { version = "2.2.2", features = [ "sqlite", "r2d2", @@ -64,7 +63,6 @@ tracing.workspace = true xmtp_cryptography = { workspace = true } xmtp_id = { path = "../xmtp_id" } xmtp_proto = { workspace = true, features = ["proto_full", "convert"] } -xmtp_v2 = { path = "../xmtp_v2" } # Test/Bench Utils anyhow = { workspace = true, optional = true } diff --git a/xmtp_mls/src/builder.rs b/xmtp_mls/src/builder.rs index 4b0e72311..16c78a84d 100644 --- a/xmtp_mls/src/builder.rs +++ b/xmtp_mls/src/builder.rs @@ -372,8 +372,11 @@ mod tests { 0, Some(legacy_key.clone()), ); - let store = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmp_path())).unwrap(); + let store = EncryptedMessageStore::new( + StorageOption::Persistent(tmp_path()), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(); let client1 = ClientBuilder::new(identity_strategy.clone()) .store(store.clone()) @@ -435,8 +438,11 @@ mod tests { let tmpdb = tmp_path(); let scw_verifier = MockSmartContractSignatureVerifier::new(true); - let store = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb)).unwrap(); + let store = EncryptedMessageStore::new( + StorageOption::Persistent(tmpdb), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(); let nonce = 0; let address = generate_local_wallet().get_address(); let inbox_id = generate_inbox_id(&address, &nonce); @@ -472,8 +478,11 @@ mod tests { let tmpdb = tmp_path(); let scw_verifier = MockSmartContractSignatureVerifier::new(true); - let store = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb)).unwrap(); + let store = EncryptedMessageStore::new( + StorageOption::Persistent(tmpdb), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(); let nonce = 0; let address = generate_local_wallet().get_address(); let inbox_id = generate_inbox_id(&address, &nonce); @@ -507,8 +516,11 @@ mod tests { let tmpdb = tmp_path(); let scw_verifier = MockSmartContractSignatureVerifier::new(true); - let store = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb)).unwrap(); + let store = EncryptedMessageStore::new( + StorageOption::Persistent(tmpdb), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(); let nonce = 0; let address = generate_local_wallet().get_address(); let inbox_id = generate_inbox_id(&address, &nonce); @@ -541,8 +553,11 @@ mod tests { let stored_inbox_id = generate_inbox_id(&address, &nonce); let tmpdb = tmp_path(); - let store = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb)).unwrap(); + let store = EncryptedMessageStore::new( + StorageOption::Persistent(tmpdb), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(); let stored: StoredIdentity = (&Identity { inbox_id: stored_inbox_id.clone(), @@ -574,11 +589,11 @@ mod tests { async fn identity_persistence_test() { let tmpdb = tmp_path(); let wallet = &generate_local_wallet(); + let db_key = EncryptedMessageStore::generate_enc_key(); // Generate a new Wallet + Store let store_a = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb.clone())) - .unwrap(); + EncryptedMessageStore::new(StorageOption::Persistent(tmpdb.clone()), db_key).unwrap(); let nonce = 1; let inbox_id = generate_inbox_id(&wallet.get_address(), &nonce); @@ -602,8 +617,7 @@ mod tests { // Reload the existing store and wallet let store_b = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb.clone())) - .unwrap(); + EncryptedMessageStore::new(StorageOption::Persistent(tmpdb.clone()), db_key).unwrap(); let client_b = ClientBuilder::new(IdentityStrategy::CreateIfNotFound( inbox_id, @@ -642,8 +656,7 @@ mod tests { // Use cached only strategy let store_d = - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb.clone())) - .unwrap(); + EncryptedMessageStore::new(StorageOption::Persistent(tmpdb.clone()), db_key).unwrap(); let client_d = ClientBuilder::new(IdentityStrategy::CachedOnly) .local_client() .await diff --git a/xmtp_mls/src/storage/encrypted_store/mod.rs b/xmtp_mls/src/storage/encrypted_store/mod.rs index 371f060c6..c117d80d8 100644 --- a/xmtp_mls/src/storage/encrypted_store/mod.rs +++ b/xmtp_mls/src/storage/encrypted_store/mod.rs @@ -21,8 +21,9 @@ pub mod identity_update; pub mod key_store_entry; pub mod refresh_state; pub mod schema; +mod sqlcipher_connection; -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; use diesel::{ connection::{AnsiTransactionManager, SimpleConnection, TransactionManager}, @@ -32,36 +33,18 @@ use diesel::{ sql_query, }; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use log::{log_enabled, warn}; use parking_lot::RwLock; -use rand::RngCore; -use xmtp_cryptography::utils as crypto_utils; use self::db_connection::DbConnection; +pub use self::sqlcipher_connection::{EncryptedConnection, EncryptionKey}; + use super::StorageError; use crate::{xmtp_openmls_provider::XmtpOpenMlsProvider, Store}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations/"); - pub type RawDbConnection = PooledConnection>; -pub type EncryptionKey = [u8; 32]; - -// For PRAGMA query log statements -#[derive(QueryableByName, Debug)] -struct CipherVersion { - #[diesel(sql_type = diesel::sql_types::Text)] - cipher_version: String, -} - -// For PRAGMA query log statements -#[derive(QueryableByName, Debug)] -struct CipherProviderVersion { - #[diesel(sql_type = diesel::sql_types::Text)] - cipher_provider_version: String, -} - // For PRAGMA query log statements #[derive(QueryableByName, Debug)] struct SqliteVersion { @@ -69,13 +52,6 @@ struct SqliteVersion { version: String, } -#[derive(Default, Clone, Debug)] -pub enum StorageOption { - #[default] - Ephemeral, - Persistent(String), -} - pub fn ignore_unique_violation( result: Result, ) -> Result<(), StorageError> { @@ -86,19 +62,49 @@ pub fn ignore_unique_violation( } } +#[derive(Default, Clone, Debug)] +pub enum StorageOption { + #[default] + Ephemeral, + Persistent(String), +} + +impl StorageOption { + // create a completely new standalone connection + fn conn(&self) -> Result { + use StorageOption::*; + match self { + Persistent(path) => SqliteConnection::establish(path), + Ephemeral => SqliteConnection::establish(":memory:"), + } + } +} + +/// An Unencrypted Connection +/// Creates a Sqlite3 Database/Connection in WAL mode. +/// Sets `busy_timeout` on each connection. +/// _*NOTE:*_Unencrypted Connections are not validated and mostly meant for testing. +/// It is not recommended to use an unencrypted connection in production. +#[derive(Clone, Debug)] +pub struct UnencryptedConnection; + +impl diesel::r2d2::CustomizeConnection + for UnencryptedConnection +{ + fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> { + conn.batch_execute("PRAGMA busy_timeout = 5000;") + .map_err(diesel::r2d2::Error::QueryError)?; + Ok(()) + } +} + #[allow(dead_code)] #[derive(Clone, Debug)] /// Manages a Sqlite db for persisting messages and other objects. pub struct EncryptedMessageStore { connect_opt: StorageOption, pool: Arc>>>>, - enc_key: Option, -} - -impl<'a> From<&'a EncryptedMessageStore> for Cow<'a, EncryptedMessageStore> { - fn from(store: &'a EncryptedMessageStore) -> Cow<'a, EncryptedMessageStore> { - Cow::Borrowed(store) - } + enc_opts: Option, } impl EncryptedMessageStore { @@ -116,62 +122,53 @@ impl EncryptedMessageStore { enc_key: Option, ) -> Result { log::info!("Setting up DB connection pool"); - let pool = - match opts { - StorageOption::Ephemeral => Pool::builder() - .max_size(1) - .build(ConnectionManager::::new(":memory:"))?, - StorageOption::Persistent(ref path) => Pool::builder() - .max_size(25) - .build(ConnectionManager::::new(path))?, - }; - - // TODO: Validate that sqlite is correctly configured. Bad EncKey is not detected until the - // migrations run which returns an unhelpful error. - let mut obj = Self { + let mut builder = Pool::builder(); + + let enc_opts = if let Some(key) = enc_key { + let enc_opts = EncryptedConnection::new(key, &opts)?; + builder = builder.connection_customizer(Box::new(enc_opts.clone())); + Some(enc_opts) + } else if matches!(opts, StorageOption::Persistent(_)) { + builder = builder.connection_customizer(Box::new(UnencryptedConnection)); + None + } else { + None + }; + + let pool = match opts { + StorageOption::Ephemeral => builder + .max_size(1) + .build(ConnectionManager::::new(":memory:"))?, + StorageOption::Persistent(ref path) => builder + .max_size(25) + .build(ConnectionManager::::new(path))?, + }; + + let mut this = Self { connect_opt: opts, pool: Arc::new(Some(pool).into()), - enc_key, + enc_opts, }; - obj.init_db()?; - Ok(obj) + this.init_db()?; + Ok(this) } fn init_db(&mut self) -> Result<(), StorageError> { - let conn = &mut self.raw_conn()?; - conn.batch_execute("PRAGMA journal_mode = WAL;") - .map_err(|e| StorageError::DbInit(e.to_string()))?; + if let Some(ref encrypted_conn) = self.enc_opts { + encrypted_conn.validate(&self.connect_opt)?; + } + let conn = &mut self.raw_conn()?; + conn.batch_execute("PRAGMA journal_mode = WAL;")?; log::info!("Running DB migrations"); conn.run_pending_migrations(MIGRATIONS) - .map_err(|e| StorageError::DbInit(e.to_string()))?; + .map_err(|e| StorageError::DbInit(format!("Failed to run migrations: {}", e)))?; let sqlite_version = sql_query("SELECT sqlite_version() AS version").load::(conn)?; log::info!("sqlite_version={}", sqlite_version[0].version); - if self.enc_key.is_some() { - let cipher_version = sql_query("PRAGMA cipher_version").load::(conn)?; - if cipher_version.is_empty() { - return Err(StorageError::SqlCipherNotLoaded); - } - let cipher_provider_version = - sql_query("PRAGMA cipher_provider_version").load::(conn)?; - log::info!( - "Sqlite cipher_version={:?}, cipher_provider_version={:?}", - cipher_version.first().as_ref().map(|v| &v.cipher_version), - cipher_provider_version - .first() - .as_ref() - .map(|v| &v.cipher_provider_version) - ); - if log_enabled!(log::Level::Info) { - conn.batch_execute("PRAGMA cipher_log = stderr; PRAGMA cipher_log_level = INFO;") - .ok(); - } - } - log::info!("Migrations successful"); Ok(()) } @@ -191,14 +188,7 @@ impl EncryptedMessageStore { pool.state().connections ); - let mut conn = pool.get()?; - if let Some(ref key) = self.enc_key { - conn.batch_execute(&format!("PRAGMA key = \"x'{}'\";", hex::encode(key)))?; - } - - conn.batch_execute("PRAGMA busy_timeout = 5000;")?; - - Ok(conn) + Ok(pool.get()?) } pub fn conn(&self) -> Result { @@ -320,13 +310,6 @@ impl EncryptedMessageStore { } } - pub fn generate_enc_key() -> EncryptionKey { - // TODO: Handle Key Better/ Zeroize - let mut key = [0u8; 32]; - crypto_utils::rng().fill_bytes(&mut key[..]); - key - } - pub fn release_connection(&self) -> Result<(), StorageError> { let mut pool_guard = self.pool.write(); pool_guard.take(); @@ -334,15 +317,20 @@ impl EncryptedMessageStore { } pub fn reconnect(&self) -> Result<(), StorageError> { - let pool = - match self.connect_opt { - StorageOption::Ephemeral => Pool::builder() - .max_size(1) - .build(ConnectionManager::::new(":memory:"))?, - StorageOption::Persistent(ref path) => Pool::builder() - .max_size(25) - .build(ConnectionManager::::new(path))?, - }; + let mut builder = Pool::builder(); + + if let Some(ref opts) = self.enc_opts { + builder = builder.connection_customizer(Box::new(opts.clone())); + } + + let pool = match self.connect_opt { + StorageOption::Ephemeral => builder + .max_size(1) + .build(ConnectionManager::::new(":memory:"))?, + StorageOption::Persistent(ref path) => builder + .max_size(25) + .build(ConnectionManager::::new(path))?, + }; let mut pool_write = self.pool.write(); *pool_write = Some(pool); @@ -354,7 +342,7 @@ impl EncryptedMessageStore { #[allow(dead_code)] fn warn_length(list: &[T], str_id: &str, max_length: usize) { if list.len() > max_length { - warn!( + log::warn!( "EncryptedStore expected at most {} {} however found {}. Using the Oldest.", max_length, str_id, @@ -448,18 +436,16 @@ where #[cfg(test)] mod tests { - use super::{ - db_connection::DbConnection, identity::StoredIdentity, EncryptedMessageStore, StorageError, - StorageOption, - }; + use super::*; use std::sync::Barrier; use crate::{ storage::group::{GroupMembershipState, StoredGroup}, + storage::identity::StoredIdentity, utils::test::{rand_vec, tmp_path}, Fetch, Store, }; - use std::{fs, sync::Arc}; + use std::sync::Arc; /// Test harness that loads an Ephemeral store. pub fn with_connection(fun: F) -> R @@ -523,8 +509,7 @@ mod tests { let fetched_identity: StoredIdentity = conn.fetch(&()).unwrap().unwrap(); assert_eq!(fetched_identity.inbox_id, inbox_id); } - - fs::remove_file(db_path).unwrap(); + EncryptedMessageStore::remove_db_files(db_path) } #[test] @@ -555,7 +540,7 @@ mod tests { assert_eq!(fetched_identity2.inbox_id, inbox_id); } - fs::remove_file(db_path).unwrap(); + EncryptedMessageStore::remove_db_files(db_path) } #[test] @@ -579,30 +564,34 @@ mod tests { // Ensure it fails assert!( - matches!(res.err(), Some(StorageError::DbInit(_))), - "Expected DbInitError" + matches!(res.err(), Some(StorageError::SqlCipherKeyIncorrect)), + "Expected SqlCipherKeyIncorrect error" ); - fs::remove_file(db_path).unwrap(); + EncryptedMessageStore::remove_db_files(db_path) } #[tokio::test] async fn encrypted_db_with_multiple_connections() { let db_path = tmp_path(); - let store = EncryptedMessageStore::new( - StorageOption::Persistent(db_path.clone()), - EncryptedMessageStore::generate_enc_key(), - ) - .unwrap(); - - let conn1 = &store.conn().unwrap(); - let inbox_id = "inbox_id"; - StoredIdentity::new(inbox_id.to_string(), rand_vec(), rand_vec()) - .store(conn1) + { + let store = EncryptedMessageStore::new( + StorageOption::Persistent(db_path.clone()), + EncryptedMessageStore::generate_enc_key(), + ) .unwrap(); - let conn2 = &store.conn().unwrap(); - let fetched_identity: StoredIdentity = conn2.fetch(&()).unwrap().unwrap(); - assert_eq!(fetched_identity.inbox_id, inbox_id); + let conn1 = &store.conn().unwrap(); + let inbox_id = "inbox_id"; + StoredIdentity::new(inbox_id.to_string(), rand_vec(), rand_vec()) + .store(conn1) + .unwrap(); + + let conn2 = &store.conn().unwrap(); + log::info!("Getting conn 2"); + let fetched_identity: StoredIdentity = conn2.fetch(&()).unwrap().unwrap(); + assert_eq!(fetched_identity.inbox_id, inbox_id); + } + EncryptedMessageStore::remove_db_files(db_path) } #[test] diff --git a/xmtp_mls/src/storage/encrypted_store/sqlcipher_connection.rs b/xmtp_mls/src/storage/encrypted_store/sqlcipher_connection.rs new file mode 100644 index 000000000..757386718 --- /dev/null +++ b/xmtp_mls/src/storage/encrypted_store/sqlcipher_connection.rs @@ -0,0 +1,352 @@ +//! SQLCipher-specific Connection +use diesel::{ + connection::{LoadConnection, SimpleConnection}, + deserialize::FromSqlRow, + prelude::*, + sql_query, +}; +use log::log_enabled; +use std::{ + fmt::Display, + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use crate::storage::StorageError; + +use super::StorageOption; + +pub type EncryptionKey = [u8; 32]; +pub type Salt = [u8; 16]; +const PLAINTEXT_HEADER_SIZE: usize = 32; +const SALT_FILE_NAME: &str = "sqlcipher_salt"; + +// For PRAGMA query log statements +#[derive(QueryableByName, Debug)] +struct CipherVersion { + #[diesel(sql_type = diesel::sql_types::Text)] + cipher_version: String, +} + +// For PRAGMA query log statements +#[derive(QueryableByName, Debug)] +struct CipherProviderVersion { + #[diesel(sql_type = diesel::sql_types::Text)] + cipher_provider_version: String, +} + +/// Specialized Connection for r2d2 connection pool. +#[derive(Clone, Debug)] +pub struct EncryptedConnection { + key: EncryptionKey, + /// We don't store the salt for Ephemeral Dbs + salt: Option, +} + +impl EncryptedConnection { + /// Creates a file for the salt and stores it + pub fn new(key: EncryptionKey, opts: &StorageOption) -> Result { + use super::StorageOption::*; + let salt = match opts { + Ephemeral => None, + Persistent(ref db_path) => { + let mut salt = [0u8; 16]; + let db_pathbuf = PathBuf::from(db_path); + let salt_path = Self::salt_file(db_path)?; + + match (salt_path.try_exists()?, db_pathbuf.try_exists()?) { + // db and salt exist + (true, true) => { + let file = File::open(salt_path)?; + salt = ::from_hex( + file.bytes().take(32).collect::, _>>()?, + )?; + } + // the db exists and needs to be migrated + (false, true) => { + log::debug!("migrating sqlcipher db to plaintext header."); + Self::migrate(db_path, key, &mut salt)?; + } + // the db doesn't exist yet and needs to be created + (false, false) => { + log::debug!("creating new sqlcipher db"); + Self::create(db_path, key, &mut salt)?; + } + // the db doesn't exist but the salt does + // This generally doesn't make sense & shouldn't happen. + // Create a new database and delete the salt file. + (true, false) => { + std::fs::remove_file(salt_path)?; + Self::create(db_path, key, &mut salt)?; + } + } + Some(salt) + } + }; + + Ok(Self { key, salt }) + } + + /// create a new database + salt file. + /// writes the 16-bytes hex-encoded salt to `salt` + fn create(path: &String, key: EncryptionKey, salt: &mut [u8]) -> Result<(), StorageError> { + let conn = &mut SqliteConnection::establish(path)?; + conn.batch_execute(&format!( + r#" + {} + {} + PRAGMA journal_mode = WAL; + "#, + pragma_key(hex::encode(key)), + pragma_plaintext_header() + ))?; + + Self::write_salt(path, conn, salt)?; + Ok(()) + } + + /// Executes the steps outlined in the [SQLCipher Docs](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size) + /// Migrates the database to `cipher_plaintext_header_size` and returns the salt after + /// persisting it to SALT_FILE_NAME. + /// + /// if the salt file already exists, deletes it. + fn migrate(path: &String, key: EncryptionKey, salt: &mut [u8]) -> Result<(), StorageError> { + let conn = &mut SqliteConnection::establish(path)?; + + conn.batch_execute(&format!( + r#" + {} + select count(*) from sqlite_master; -- trigger header read, currently it is encrypted + "#, + pragma_key(hex::encode(key)) + ))?; + + // get the salt and save it for later use + Self::write_salt(path, conn, salt)?; + + conn.batch_execute(&format!( + r#" + {} + PRAGMA user_version = 1; -- force header write + "#, + pragma_plaintext_header() + ))?; + + Ok(()) + } + + /// Get the salt from the opened database, write it to `Self::salt_file(db_path)` as hex-encoded + /// bytes, and then copy it to `buf` after decoding hex bytes. + fn write_salt( + path: &String, + conn: &mut SqliteConnection, + buf: &mut [u8], + ) -> Result<(), StorageError> { + let mut row_iter = conn.load(sql_query("PRAGMA cipher_salt"))?; + // cipher salt should always exist. if it doesn't SQLCipher is misconfigured. + let row = row_iter.next().ok_or(StorageError::NotFound( + "Cipher salt doesn't exist in database".into(), + ))??; + let salt = >::build_from_row(&row)?; + log::debug!( + "writing salt={} to file {:?}", + salt, + Self::salt_file(PathBuf::from(path))?, + ); + let mut f = File::create(Self::salt_file(PathBuf::from(path))?)?; + + f.write_all(salt.as_bytes())?; + let mut perms = f.metadata()?.permissions(); + perms.set_readonly(true); + f.set_permissions(perms)?; + + let salt = hex::decode(salt)?; + buf.copy_from_slice(&salt); + Ok(()) + } + + /// Salt file is stored next to the sqlite3 db3 file as `{db_file_name}.SALT_FILE_NAME`. + /// If the db file is named `sqlite3_xmtp_db.db3`, the salt file would + /// be stored next to this file as `sqlite3_xmtp_db.db3.sqlcipher_salt` + pub(crate) fn salt_file>(db_path: P) -> std::io::Result { + let db_path: &Path = db_path.as_ref(); + let name = db_path.file_name().ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "database file has no name", + ))?; + let db_path = db_path.parent().ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Parent directory could not be found", + ))?; + Ok(db_path.join(format!("{}.{}", name.to_string_lossy(), SALT_FILE_NAME))) + } + + pub(super) fn validate(&self, opts: &StorageOption) -> Result<(), StorageError> { + let conn = &mut opts.conn()?; + + let cipher_version = sql_query("PRAGMA cipher_version").load::(conn)?; + if cipher_version.is_empty() { + return Err(StorageError::SqlCipherNotLoaded); + } + + // test the key according to + // https://www.zetetic.net/sqlcipher/sqlcipher-api/#testing-the-key + conn.batch_execute(&format!( + "{} + SELECT count(*) FROM sqlite_master;", + self.pragmas() + )) + .map_err(|_| StorageError::SqlCipherKeyIncorrect)?; + + let CipherProviderVersion { + cipher_provider_version, + } = sql_query("PRAGMA cipher_provider_version") + .get_result::(conn)?; + log::info!( + "Sqlite cipher_version={:?}, cipher_provider_version={:?}", + cipher_version.first().as_ref().map(|v| &v.cipher_version), + cipher_provider_version + ); + + if log_enabled!(log::Level::Info) { + conn.batch_execute("PRAGMA cipher_log = stderr; PRAGMA cipher_log_level = INFO;") + .ok(); + } else { + conn.batch_execute("PRAGMA cipher_log = stderr; PRAGMA cipher_log_level = WARN;") + .ok(); + } + log::debug!("SQLCipher Database validated."); + Ok(()) + } + + /// Output the corect order of PRAGMAS to instantiate a connection + fn pragmas(&self) -> impl Display { + let Self { ref key, ref salt } = self; + + if let Some(s) = salt { + format!( + "{}\n{}\n{}", + pragma_key(hex::encode(key)), + pragma_plaintext_header(), + pragma_salt(hex::encode(s)) + ) + } else { + format!( + "{}\n{}", + pragma_key(hex::encode(key)), + pragma_plaintext_header() + ) + } + } +} + +impl diesel::r2d2::CustomizeConnection + for EncryptedConnection +{ + fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> { + conn.batch_execute(&format!( + "{} + PRAGMA busy_timeout = 5000;", + self.pragmas() + )) + .map_err(diesel::r2d2::Error::QueryError)?; + + Ok(()) + } +} + +fn pragma_key(key: impl Display) -> impl Display { + format!(r#"PRAGMA key = "x'{key}'";"#) +} + +fn pragma_salt(salt: impl Display) -> impl Display { + format!(r#"PRAGMA cipher_salt="x'{salt}'";"#) +} + +fn pragma_plaintext_header() -> impl Display { + format!(r#"PRAGMA cipher_plaintext_header_size={PLAINTEXT_HEADER_SIZE};"#) +} + +#[cfg(test)] +mod tests { + use crate::{storage::EncryptedMessageStore, utils::test::tmp_path}; + use diesel_migrations::MigrationHarness; + use std::fs::File; + + use super::*; + const SQLITE3_PLAINTEXT_HEADER: &str = "SQLite format 3\0"; + use StorageOption::*; + + #[test] + fn test_db_creates_with_plaintext_header() { + let db_path = tmp_path(); + { + let _ = EncryptedMessageStore::new( + Persistent(db_path.clone()), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(); + + assert!(EncryptedConnection::salt_file(&db_path).unwrap().exists()); + let bytes = std::fs::read(EncryptedConnection::salt_file(&db_path).unwrap()).unwrap(); + let salt = hex::decode(bytes).unwrap(); + assert_eq!(salt.len(), 16); + + let mut plaintext_header = [0; 16]; + let mut file = File::open(&db_path).unwrap(); + file.read_exact(&mut plaintext_header).unwrap(); + + assert_eq!( + SQLITE3_PLAINTEXT_HEADER, + String::from_utf8(plaintext_header.into()).unwrap() + ); + } + EncryptedMessageStore::remove_db_files(db_path) + } + + #[test] + fn test_db_migrates() { + let db_path = tmp_path(); + { + let key = EncryptedMessageStore::generate_enc_key(); + { + let conn = &mut SqliteConnection::establish(&db_path).unwrap(); + conn.batch_execute(&format!( + r#" + {} + PRAGMA busy_timeout = 5000; + PRAGMA journal_mode = WAL; + "#, + pragma_key(hex::encode(key)) + )) + .unwrap(); + conn.run_pending_migrations(crate::storage::MIGRATIONS) + .unwrap(); + } + + // no plaintext header before migration + let mut plaintext_header = [0; 16]; + let mut file = File::open(&db_path).unwrap(); + file.read_exact(&mut plaintext_header).unwrap(); + assert!(String::from_utf8_lossy(&plaintext_header) != SQLITE3_PLAINTEXT_HEADER); + + let _ = EncryptedMessageStore::new(Persistent(db_path.clone()), key).unwrap(); + + assert!(EncryptedConnection::salt_file(&db_path).unwrap().exists()); + let bytes = std::fs::read(EncryptedConnection::salt_file(&db_path).unwrap()).unwrap(); + let salt = hex::decode(bytes).unwrap(); + assert_eq!(salt.len(), 16); + + let mut plaintext_header = [0; 16]; + let mut file = File::open(&db_path).unwrap(); + file.read_exact(&mut plaintext_header).unwrap(); + + assert_eq!( + SQLITE3_PLAINTEXT_HEADER, + String::from_utf8(plaintext_header.into()).unwrap() + ); + } + EncryptedMessageStore::remove_db_files(db_path) + } +} diff --git a/xmtp_mls/src/storage/errors.rs b/xmtp_mls/src/storage/errors.rs index 7e6b914f3..e56f6f772 100644 --- a/xmtp_mls/src/storage/errors.rs +++ b/xmtp_mls/src/storage/errors.rs @@ -37,6 +37,12 @@ pub enum StorageError { Intent(#[from] IntentError), #[error("The SQLCipher Sqlite extension is not present, but an encryption key is given")] SqlCipherNotLoaded, + #[error("PRAGMA key or salt has incorrect value")] + SqlCipherKeyIncorrect, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + FromHex(#[from] hex::FromHexError), } impl From> for StorageError { @@ -69,6 +75,7 @@ impl RetryableError for StorageError { Self::Lock(_) => true, Self::SqlCipherNotLoaded => true, Self::PoolNeedsConnection => true, + Self::SqlCipherKeyIncorrect => false, _ => false, } } diff --git a/xmtp_mls/src/subscriptions.rs b/xmtp_mls/src/subscriptions.rs index 13cfc9657..2bb6f8cc4 100644 --- a/xmtp_mls/src/subscriptions.rs +++ b/xmtp_mls/src/subscriptions.rs @@ -244,6 +244,7 @@ where while let Some(convo) = stream.next().await { convo_callback(convo) } + log::debug!("`stream_conversations` stream ended, dropping stream"); Ok(()) }); @@ -268,6 +269,7 @@ where while let Some(message) = stream.next().await { callback(message) } + log::debug!("`stream_messages` stream ended, dropping stream"); Ok(()) }); @@ -370,6 +372,7 @@ where Err(m) => log::error!("error during stream all messages {}", m), } } + log::debug!("`stream_all_messages` stream ended, dropping stream"); Ok(()) }); @@ -700,7 +703,7 @@ mod tests { .unwrap(); } - let _ = tokio::time::timeout(std::time::Duration::from_secs(120), async { + let _ = tokio::time::timeout(std::time::Duration::from_secs(60), async { while blocked.load(Ordering::SeqCst) > 0 { tokio::task::yield_now().await; } diff --git a/xmtp_mls/src/utils/test.rs b/xmtp_mls/src/utils/test.rs index fbbd190ce..529191155 100755 --- a/xmtp_mls/src/utils/test.rs +++ b/xmtp_mls/src/utils/test.rs @@ -3,7 +3,7 @@ use std::env; use rand::{ distributions::{Alphanumeric, DistString}, - Rng, + Rng, RngCore, }; use std::sync::Arc; use tokio::{sync::Notify, time::error::Elapsed}; @@ -17,7 +17,7 @@ use xmtp_id::associations::{ use crate::{ builder::ClientBuilder, identity::IdentityStrategy, - storage::{EncryptedMessageStore, StorageOption}, + storage::{EncryptedConnection, EncryptedMessageStore, EncryptionKey, StorageOption}, types::Address, Client, InboxOwner, XmtpApi, XmtpTestClient, }; @@ -78,11 +78,29 @@ impl XmtpTestClient for GrpcClient { } } +impl EncryptedMessageStore { + pub fn generate_enc_key() -> EncryptionKey { + let mut key = [0u8; 32]; + xmtp_cryptography::utils::rng().fill_bytes(&mut key[..]); + key + } + + pub fn remove_db_files>(path: P) { + let path = path.as_ref(); + std::fs::remove_file(path).unwrap(); + std::fs::remove_file(EncryptedConnection::salt_file(path).unwrap()).unwrap(); + } +} + impl ClientBuilder { pub fn temp_store(self) -> Self { let tmpdb = tmp_path(); self.store( - EncryptedMessageStore::new_unencrypted(StorageOption::Persistent(tmpdb)).unwrap(), + EncryptedMessageStore::new( + StorageOption::Persistent(tmpdb), + EncryptedMessageStore::generate_enc_key(), + ) + .unwrap(), ) } diff --git a/xmtp_proto/Cargo.toml b/xmtp_proto/Cargo.toml index 60dc6c1cf..9bfffc877 100644 --- a/xmtp_proto/Cargo.toml +++ b/xmtp_proto/Cargo.toml @@ -5,14 +5,12 @@ version.workspace = true [dependencies] futures = { workspace = true } -futures-core = { workspace = true } pbjson-types.workspace = true pbjson.workspace = true prost = { workspace = true, features = ["prost-derive"] } # Only necessary if using Protobuf well-known types: prost-types = { workspace = true } serde = { workspace = true } -openmls_basic_credential = { workspace = true, optional = true } openmls = { workspace = true, optional = true } trait-variant = "0.1.2" @@ -21,7 +19,7 @@ tonic = { workspace = true } [features] -convert = ["openmls_basic_credential", "openmls", "proto_full"] +convert = ["openmls", "proto_full"] default = [] # @@protoc_deletion_point(features) diff --git a/xmtp_user_preferences/Cargo.toml b/xmtp_user_preferences/Cargo.toml index 11f887f8d..7f7ba0f27 100644 --- a/xmtp_user_preferences/Cargo.toml +++ b/xmtp_user_preferences/Cargo.toml @@ -4,9 +4,8 @@ name = "xmtp_user_preferences" version.workspace = true [dependencies] -base64 = "0.21.4" +base64 = "0.22.1" # Need to include this as a dep or compile will fail because of a version mismatch -once_cell = "1.18.0" prost = { workspace = true, features = ["prost-derive"] } xmtp_proto = { path = "../xmtp_proto", features = ["xmtp-message_contents"] } xmtp_v2 = { path = "../xmtp_v2" }