Skip to content

Commit

Permalink
Provide HMAC Keys (#1394)
Browse files Browse the repository at this point in the history
* hkdf wip

* calculate the kdf

* hash the message, provide the hmac

* missing files

* lint

* provide on the ffi

* needless bit flipping

* test fix

* lint

* lint

* bump version

* test it

* move down

* typo

* update hmac algo

* lint

* move the salt const
  • Loading branch information
codabrink authored Dec 10, 2024
1 parent ca27157 commit 0d7674e
Show file tree
Hide file tree
Showing 20 changed files with 226 additions and 56 deletions.
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/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ pub const DEFAULT_GROUP_PINNED_FRAME_URL: &str = "";
// If a metadata field name starts with this character,
// and it does not have a policy set, it is a super admin only field
pub const SUPER_ADMIN_METADATA_PREFIX: &str = "_";
pub(crate) const HMAC_SALT: &[u8] = b"libXMTP HKDF salt!";
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
File renamed without changes.
103 changes: 95 additions & 8 deletions xmtp_mls/src/groups/mls_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ 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,
GRPC_DATA_LIMIT, HMAC_SALT, MAX_GROUP_SIZE, MAX_INTENT_PUBLISH_ATTEMPTS, MAX_PAST_EPOCHS,
SYNC_UPDATE_INSTALLATIONS_INTERVAL_NS,
},
groups::{intents::UpdateMetadataIntentData, validated_commit::ValidatedCommit},
Expand All @@ -28,14 +28,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 +57,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 +1028,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 +1392,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 +1630,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

0 comments on commit 0d7674e

Please sign in to comment.