Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide HMAC Keys #1394

Merged
merged 20 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ futures = "0.3.30"
futures-core = "0.3.30"
getrandom = { version = "0.2", default-features = false }
hex = "0.4.3"
hkdf = "0.12.3"
openmls = { git = "https://github.com/xmtp/openmls", rev = "043b347cb18d528647df36f500725ab57c41c7db", default-features = false }
openmls_basic_credential = { git = "https://github.com/xmtp/openmls", rev = "043b347cb18d528647df36f500725ab57c41c7db" }
openmls_rust_crypto = { git = "https://github.com/xmtp/openmls", rev = "043b347cb18d528647df36f500725ab57c41c7db" }
Expand Down
34 changes: 34 additions & 0 deletions bindings_ffi/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use xmtp_id::{
InboxId,
};
use xmtp_mls::groups::scoped_client::LocalScopedGroupClient;
use xmtp_mls::groups::HmacKey;
use xmtp_mls::storage::group::ConversationType;
use xmtp_mls::storage::group_message::MsgQueryArgs;
use xmtp_mls::storage::group_message::SortDirection;
Expand Down Expand Up @@ -527,6 +528,33 @@ impl FfiXmtpClient {
scw_verifier: self.inner_client.scw_verifier().clone().clone(),
}))
}

pub fn get_hmac_keys(&self) -> Result<Vec<FfiHmacKey>, GenericError> {
let inner = self.inner_client.as_ref();
let conversations = inner.find_groups(GroupQueryArgs::default())?;

let mut keys = vec![];
for conversation in conversations {
let mut k = conversation
.hmac_keys(-1..=1)?
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();

keys.append(&mut k);
}

Ok(keys)
}
}

impl From<HmacKey> for FfiHmacKey {
fn from(value: HmacKey) -> Self {
Self {
epoch: value.epoch,
key: value.key.to_vec(),
}
}
}

#[derive(uniffi::Record)]
Expand All @@ -537,6 +565,12 @@ pub struct FfiInboxState {
pub account_addresses: Vec<String>,
}

#[derive(uniffi::Record)]
pub struct FfiHmacKey {
key: Vec<u8>,
epoch: i64,
}

#[derive(uniffi::Record)]
pub struct FfiInstallation {
pub id: Vec<u8>,
Expand Down
3 changes: 3 additions & 0 deletions xmtp_mls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ bincode.workspace = true
diesel_migrations.workspace = true
futures.workspace = true
hex.workspace = true
hkdf.workspace = true
openmls_rust_crypto = { workspace = true }
openmls_traits = { workspace = true }
parking_lot.workspace = true
Expand All @@ -59,6 +60,7 @@ rand = { workspace = true }
reqwest = { version = "0.12.4", features = ["stream"] }
serde = { workspace = true }
serde_json.workspace = true
sha2.workspace = true
thiserror = { workspace = true }
tls_codec = { workspace = true }
tokio-stream = { version = "0.1", default-features = false, features = [
Expand Down Expand Up @@ -90,6 +92,7 @@ criterion = { version = "0.5", features = [
"html_reports",
"async_tokio",
], optional = true }
hmac = "0.12.1"
indicatif = { version = "0.17", optional = true }
mockall = { version = "0.13.1", optional = true }
once_cell = { version = "1.19", optional = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE user_preferences DROP COLUMN hmac_key;
ALTER TABLE user_preferences ADD COLUMN hmac_key BLOB;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE user_preferences DROP COLUMN hmac_key;
ALTER TABLE user_preferences ADD COLUMN hmac_key BLOB NOT NULL;
27 changes: 10 additions & 17 deletions xmtp_mls/src/api/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ use super::ApiClientWrapper;
use crate::{retry_async, XmtpApi};
use xmtp_proto::api_client::XmtpMlsStreams;
use xmtp_proto::xmtp::mls::api::v1::{
group_message_input::{Version as GroupMessageInputVersion, V1 as GroupMessageInputV1},
subscribe_group_messages_request::Filter as GroupFilterProto,
subscribe_welcome_messages_request::Filter as WelcomeFilterProto,
FetchKeyPackagesRequest, GroupMessage, GroupMessageInput, KeyPackageUpload, PagingInfo,
QueryGroupMessagesRequest, QueryWelcomeMessagesRequest, SendGroupMessagesRequest,
SendWelcomeMessagesRequest, SortDirection, SubscribeGroupMessagesRequest,
SubscribeWelcomeMessagesRequest, UploadKeyPackageRequest, WelcomeMessage, WelcomeMessageInput,
subscribe_welcome_messages_request::Filter as WelcomeFilterProto, FetchKeyPackagesRequest,
GroupMessage, GroupMessageInput, KeyPackageUpload, PagingInfo, QueryGroupMessagesRequest,
QueryWelcomeMessagesRequest, SendGroupMessagesRequest, SendWelcomeMessagesRequest,
SortDirection, SubscribeGroupMessagesRequest, SubscribeWelcomeMessagesRequest,
UploadKeyPackageRequest, WelcomeMessage, WelcomeMessageInput,
};
use xmtp_proto::{Error as ApiError, ErrorKind};

Expand Down Expand Up @@ -250,28 +249,22 @@ where
}

#[tracing::instrument(level = "trace", skip_all)]
pub async fn send_group_messages(&self, group_messages: Vec<&[u8]>) -> Result<(), ApiError> {
pub async fn send_group_messages(
&self,
group_messages: Vec<GroupMessageInput>,
) -> Result<(), ApiError> {
tracing::debug!(
inbox_id = self.inbox_id,
"sending [{}] group messages",
group_messages.len()
);
let to_send: Vec<GroupMessageInput> = group_messages
.iter()
.map(|msg| GroupMessageInput {
version: Some(GroupMessageInputVersion::V1(GroupMessageInputV1 {
data: msg.to_vec(),
sender_hmac: vec![],
})),
})
.collect();

retry_async!(
self.retry_strategy,
(async {
self.api_client
.send_group_messages(SendGroupMessagesRequest {
messages: to_send.clone(),
messages: group_messages.clone(),
})
.await
})
Expand Down
6 changes: 4 additions & 2 deletions xmtp_mls/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ use xmtp_proto::xmtp::mls::api::v1::{

use crate::{
api::ApiClientWrapper,
groups::{group_permissions::PolicySet, GroupError, GroupMetadataOptions, MlsGroup},
groups::{
device_sync::preference_sync::UserPreferenceUpdate, group_permissions::PolicySet,
GroupError, GroupMetadataOptions, MlsGroup,
},
identity::{parse_credential, Identity, IdentityError},
identity_updates::{load_identity_updates, IdentityUpdateError},
intents::ProcessIntentError,
mutex_registry::MutexRegistry,
preferences::UserPreferenceUpdate,
retry::Retry,
retry_async, retryable,
storage::{
Expand Down
1 change: 1 addition & 0 deletions xmtp_mls/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) const HMAC_SALT: &[u8] = b"libXMTP HKDF salt!";
codabrink marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion xmtp_mls/src/groups/device_sync.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::{GroupError, MlsGroup};
use crate::configuration::NS_IN_HOUR;
use crate::preferences::UserPreferenceUpdate;
use crate::retry::{Retry, RetryableError};
use crate::storage::group::{ConversationType, GroupQueryArgs};
use crate::storage::group_message::MsgQueryArgs;
Expand All @@ -25,6 +24,7 @@ use aes_gcm::{
Aes256Gcm,
};
use futures::{Stream, StreamExt};
use preference_sync::UserPreferenceUpdate;
use rand::{
distributions::{Alphanumeric, DistString},
Rng, RngCore,
Expand All @@ -50,6 +50,7 @@ use xmtp_proto::xmtp::mls::message_contents::{

pub mod consent_sync;
pub mod message_sync;
pub mod preference_sync;

pub const ENC_KEY_SIZE: usize = 32; // 256-bit key
pub const NONCE_SIZE: usize = 12; // 96-bit nonce
Expand Down
2 changes: 1 addition & 1 deletion xmtp_mls/src/groups/device_sync/consent_sync.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use crate::{preferences::UserPreferenceUpdate, Client, XmtpApi};
use crate::{Client, XmtpApi};
use xmtp_id::scw_verifier::SmartContractSignatureVerifier;
use xmtp_proto::xmtp::mls::message_contents::UserPreferenceUpdate as UserPreferenceUpdateProto;

Expand Down
102 changes: 95 additions & 7 deletions xmtp_mls/src/groups/mls_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ use super::{
UpdateAdminListIntentData, UpdateGroupMembershipIntentData, UpdatePermissionIntentData,
},
validated_commit::{extract_group_membership, CommitValidationError},
GroupError, IntentError, MlsGroup, ScopedGroupClient,
GroupError, HmacKey, IntentError, MlsGroup, ScopedGroupClient,
};
use crate::{
codecs::{group_updated::GroupUpdatedCodec, ContentCodec},
configuration::{
GRPC_DATA_LIMIT, MAX_GROUP_SIZE, MAX_INTENT_PUBLISH_ATTEMPTS, MAX_PAST_EPOCHS,
SYNC_UPDATE_INSTALLATIONS_INTERVAL_NS,
},
constants::HMAC_SALT,
groups::{intents::UpdateMetadataIntentData, validated_commit::ValidatedCommit},
hpke::{encrypt_welcome, HpkeError},
identity::{parse_credential, IdentityError},
Expand All @@ -28,14 +29,18 @@ use crate::{
refresh_state::EntityKind,
serialization::{db_deserialize, db_serialize},
sql_key_store,
user_preferences::StoredUserPreferences,
StorageError,
},
subscriptions::LocalEvents,
utils::{hash::sha256, id::calculate_message_id},
utils::{hash::sha256, id::calculate_message_id, time::hmac_epoch},
xmtp_openmls_provider::XmtpOpenMlsProvider,
Delete, Fetch, StoreOrIgnore,
};
use crate::{groups::device_sync::DeviceSyncContent, subscriptions::SyncMessage};
use futures::future::try_join_all;
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use openmls::{
credentials::BasicCredential,
extensions::Extensions,
Expand All @@ -53,19 +58,22 @@ use openmls::{framing::WireFormat, prelude::BasicCredentialError};
use openmls_traits::{signatures::Signer, OpenMlsProvider};
use prost::bytes::Bytes;
use prost::Message;
use sha2::Sha256;
use std::{
collections::{HashMap, HashSet},
mem::{discriminant, Discriminant},
ops::RangeInclusive,
};
use thiserror::Error;
use xmtp_id::{InboxId, InboxIdRef};
use xmtp_proto::xmtp::mls::{
api::v1::{
group_message::{Version as GroupMessageVersion, V1 as GroupMessageV1},
group_message_input::{Version as GroupMessageInputVersion, V1 as GroupMessageInputV1},
welcome_message_input::{
Version as WelcomeMessageInputVersion, V1 as WelcomeMessageInputV1,
},
GroupMessage, WelcomeMessageInput,
GroupMessage, GroupMessageInput, WelcomeMessageInput,
},
message_contents::{
plaintext_envelope::{v2::MessageType, Content, V1, V2},
Expand Down Expand Up @@ -1021,10 +1029,8 @@ where
intent.id
);

self.client
.api()
.send_group_messages(vec![payload_slice])
.await?;
let messages = self.prepare_group_messages(vec![payload_slice])?;
self.client.api().send_group_messages(messages).await?;

tracing::info!(
intent.id,
Expand Down Expand Up @@ -1387,6 +1393,62 @@ where
try_join_all(futures).await?;
Ok(())
}

/// Provides hmac keys for a range of epochs around current epoch
/// `group.hmac_keys(-1..=1)`` will provide 3 keys consisting of last epoch, current epoch, and next epoch
/// `group.hmac_keys(0..=0) will provide 1 key, consisting of only the current epoch
#[tracing::instrument(level = "trace", skip_all)]
pub fn hmac_keys(
&self,
epoch_delta_range: RangeInclusive<i64>,
) -> Result<Vec<HmacKey>, StorageError> {
let conn = self.client.store().conn()?;
let mut ikm = StoredUserPreferences::load(&conn)?.hmac_key;
ikm.extend(&self.group_id);
let hkdf = Hkdf::<Sha256>::new(Some(HMAC_SALT), &ikm[..]);

let mut result = vec![];
let current_epoch = hmac_epoch();
for delta in epoch_delta_range {
let mut key = [0; 42];
let epoch = current_epoch + delta;
hkdf.expand(&epoch.to_le_bytes(), &mut key)
.expect("Length is correct");

result.push(HmacKey { key, epoch });
}

Ok(result)
}

#[tracing::instrument(level = "trace", skip_all)]
pub(super) fn prepare_group_messages(
&self,
payloads: Vec<&[u8]>,
) -> Result<Vec<GroupMessageInput>, GroupError> {
let hmac_key = self
.hmac_keys(0..=0)?
.pop()
.expect("Range of count 1 was provided.");
let sender_hmac =
Hmac::<Sha256>::new_from_slice(&hmac_key.key).expect("HMAC can take key of any size");

let mut result = vec![];
for payload in payloads {
let mut sender_hmac = sender_hmac.clone();
sender_hmac.update(payload);
let sender_hmac = sender_hmac.finalize();

result.push(GroupMessageInput {
version: Some(GroupMessageInputVersion::V1(GroupMessageInputV1 {
data: payload.to_vec(),
sender_hmac: sender_hmac.into_bytes().to_vec(),
})),
});
}

Ok(result)
}
}

// Extracts the message sender, but does not do any validation to ensure that the
Expand Down Expand Up @@ -1569,4 +1631,30 @@ pub(crate) mod tests {
}
future::join_all(futures).await;
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test(flavor = "multi_thread"))]
async fn hmac_keys_work_as_expected() {
let wallet = generate_local_wallet();
let amal = Arc::new(ClientBuilder::new_test_client(&wallet).await);
let amal_group: Arc<MlsGroup<_>> =
Arc::new(amal.create_group(None, Default::default()).unwrap());

let hmac_keys = amal_group.hmac_keys(-1..=1).unwrap();
let current_hmac_key = amal_group.hmac_keys(0..=0).unwrap().pop().unwrap();
assert_eq!(hmac_keys.len(), 3);
assert_eq!(hmac_keys[1].key, current_hmac_key.key);
assert_eq!(hmac_keys[1].epoch, current_hmac_key.epoch);

// Make sure the keys are different
assert_ne!(hmac_keys[0].key, hmac_keys[1].key);
assert_ne!(hmac_keys[0].key, hmac_keys[2].key);
assert_ne!(hmac_keys[1].key, hmac_keys[2].key);

// Make sure the epochs align
let current_epoch = hmac_epoch();
assert_eq!(hmac_keys[0].epoch, current_epoch - 1);
assert_eq!(hmac_keys[1].epoch, current_epoch);
assert_eq!(hmac_keys[2].epoch, current_epoch + 1);
}
}
Loading