From 756d50737e5693708d30d1a03a6607e059a7dee0 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Wed, 6 Aug 2025 14:16:49 +0100 Subject: [PATCH] feat(crypto): Add state event encryption methods to OlmMachine Signed-off-by: Skye Elliot --- crates/matrix-sdk-crypto/src/machine/mod.rs | 64 ++++++++++++++ .../src/machine/tests/mod.rs | 83 +++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 6346dfdac..30553d744 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "experimental-encrypted-state-events")] +use std::borrow::Borrow; use std::{ collections::{BTreeMap, HashMap, HashSet}, sync::Arc, @@ -31,6 +33,8 @@ use matrix_sdk_common::{ locks::RwLock as StdRwLock, BoxFuture, }; +#[cfg(feature = "experimental-encrypted-state-events")] +use ruma::events::{AnyStateEventContent, StateEventContent}; use ruma::{ api::client::{ dehydrated_device::DehydratedDeviceData, @@ -1102,6 +1106,66 @@ impl OlmMachine { self.inner.group_session_manager.encrypt(room_id, event_type, content).await } + /// Encrypt a state event for the given room. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the event should be + /// encrypted. + /// + /// * `content` - The plaintext content of the event that should be + /// encrypted. + /// + /// * `state_key` - The associated state key of the event. + #[cfg(feature = "experimental-encrypted-state-events")] + pub async fn encrypt_state_event( + &self, + room_id: &RoomId, + content: C, + state_key: K, + ) -> MegolmResult> + where + C: StateEventContent, + C::StateKey: Borrow, + K: AsRef, + { + let event_type = content.event_type().to_string(); + let content = Raw::new(&content)?.cast_unchecked(); + self.encrypt_state_event_raw(room_id, &event_type, state_key.as_ref(), &content).await + } + + /// Encrypt a state event for the given state event using its raw JSON + /// content and state key. + /// + /// This method is equivalent to [`OlmMachine::encrypt_state_event`] + /// method but operates on an arbitrary JSON value instead of strongly-typed + /// event content struct. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room for which the message should be + /// encrypted. + /// + /// * `event_type` - The type of the event. + /// + /// * `state_key` - The associated state key of the event. + /// + /// * `content` - The plaintext content of the event that should be + /// encrypted as a raw JSON value. + #[cfg(feature = "experimental-encrypted-state-events")] + pub async fn encrypt_state_event_raw( + &self, + room_id: &RoomId, + event_type: &str, + state_key: &str, + content: &Raw, + ) -> MegolmResult> { + self.inner + .group_session_manager + .encrypt_state(room_id, event_type, state_key, content) + .await + } + /// Forces the currently active room key, which is used to encrypt messages, /// to be rotated. /// diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index e96493f36..ded08c1d0 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -26,6 +26,11 @@ use matrix_sdk_common::{ executor::spawn, }; use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json}; +#[cfg(feature = "experimental-encrypted-state-events")] +use ruma::events::{ + room::topic::{OriginalRoomTopicEvent, RoomTopicEventContent}, + StateEvent, +}; use ruma::{ api::client::{ keys::{get_keys, upload_keys}, @@ -727,6 +732,84 @@ async fn test_megolm_encryption() { } } +#[cfg(feature = "experimental-encrypted-state-events")] +#[async_test] +async fn test_megolm_state_encryption() { + use ruma::events::{AnyStateEvent, EmptyStateKey}; + + let (alice, bob) = + get_machine_pair_with_setup_sessions_test_helper(alice_id(), user_id(), false).await; + let room_id = room_id!("!test:example.org"); + + let to_device_requests = alice + .share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default()) + .await + .unwrap(); + + let event = ToDeviceEvent::new( + alice.user_id().to_owned(), + to_device_requests_to_content(to_device_requests), + ); + + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let group_session = bob + .store() + .with_transaction(|mut tr| async { + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; + Ok((tr, res)) + }) + .await + .unwrap() + .inbound_group_session + .unwrap(); + let sessions = std::slice::from_ref(&group_session); + bob.store().save_inbound_group_sessions(sessions).await.unwrap(); + + let plaintext = "It is a secret to everybody"; + + let content = RoomTopicEventContent::new(plaintext.to_owned()); + + let encrypted_content = + alice.encrypt_state_event(room_id, content, EmptyStateKey).await.unwrap(); + + let event = json!({ + "event_id": "$xxxxx:example.org", + "origin_server_ts": MilliSecondsSinceUnixEpoch::now(), + "sender": alice.user_id(), + "type": "m.room.encrypted", + "content": encrypted_content, + }); + + let event = json_convert(&event).unwrap(); + + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let decryption_result = + bob.try_decrypt_room_event(&event, room_id, &decryption_settings).await.unwrap(); + assert_let!(RoomEventDecryptionResult::Decrypted(decrypted_event) = decryption_result); + let decrypted_event = decrypted_event.event.deserialize().unwrap(); + + if let AnyTimelineEvent::State(AnyStateEvent::RoomTopic(StateEvent::Original( + OriginalRoomTopicEvent { sender, content, .. }, + ))) = decrypted_event + { + assert_eq!(&sender, alice.user_id()); + assert_eq!(&content.topic, plaintext); + } else { + panic!("Decrypted room event has the wrong type"); + } +} + #[async_test] async fn test_withheld_unverified() { let (alice, bob) =