feat(crypto): Add state event encryption methods to OlmMachine

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
Skye Elliot
2025-08-06 14:16:49 +01:00
parent c32877284c
commit 756d50737e
2 changed files with 147 additions and 0 deletions

View File

@@ -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<C, K>(
&self,
room_id: &RoomId,
content: C,
state_key: K,
) -> MegolmResult<Raw<RoomEncryptedEventContent>>
where
C: StateEventContent,
C::StateKey: Borrow<K>,
K: AsRef<str>,
{
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<AnyStateEventContent>,
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
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.
///

View File

@@ -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) =