crypto: Add variants for plain text and encrypted to-device events

fixup: post rebase
This commit is contained in:
Valere
2025-04-15 11:08:48 +02:00
committed by Damir Jelić
parent 3b133865f0
commit 4363105976
10 changed files with 314 additions and 30 deletions

View File

@@ -554,8 +554,10 @@ impl OlmMachine {
}),
)?;
let to_device_events =
to_device_events.into_iter().map(|event| event.json().get().to_owned()).collect();
let to_device_events = to_device_events
.into_iter()
.map(|event| event.to_raw().json().get().to_owned())
.collect();
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
Ok(SyncChangesResult { to_device_events, room_key_infos })
@@ -930,6 +932,8 @@ impl OlmMachine {
},
}
}
// Should not happen
_ => panic!("Unsupported algorithm in room"),
})
}

View File

@@ -99,6 +99,15 @@ async fn process(
let (events, room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
let events = events
.iter()
// XXX There is loss of information here, after calling `to_raw` it is not
// possible to make the difference between a successfully decrypted event and a plain
// text event. This information needs to be propagated to top layer at some point if
// clients relies on custom encrypted to device events.
.map(|p| p.to_raw())
.collect();
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
} else {
// If we have no `OlmMachine`, just return the events that were passed in.

View File

@@ -17,7 +17,7 @@ use std::{collections::BTreeMap, fmt};
#[cfg(doc)]
use ruma::events::AnyTimelineEvent;
use ruma::{
events::{AnyMessageLikeEvent, AnySyncTimelineEvent},
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyToDeviceEvent},
push::Action,
serde::{
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
@@ -282,6 +282,11 @@ pub enum AlgorithmInfo {
/// key.
sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String>,
},
OlmV1Curve25519AesSha2 {
// The sender key of the device that encrypted the message
curve25519_key: String,
},
}
/// Struct containing information on how an event was decrypted.
@@ -615,6 +620,40 @@ impl fmt::Debug for DecryptedRoomEvent {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ProcessedToDeviceEvent {
/// A successfully-decrypted encrypted event.
Decrypted {
/// The decrypted to device event
decrypted_event: Raw<AnyToDeviceEvent>,
/// Sender encryption information
encryption_info: EncryptionInfo,
},
/// An encrypted event which could not be decrypted.
UnableToDecrypt {
/// The `m.room.encrypted` to device event.
event: Raw<AnyToDeviceEvent>,
},
/// An unencrypted event.
PlainText(Raw<AnyToDeviceEvent>),
/// An invalid to device event that was ignored
NotProcessed(Raw<AnyToDeviceEvent>),
}
impl ProcessedToDeviceEvent {
pub fn to_raw(&self) -> Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { decrypted_event, .. } => decrypted_event.clone(),
ProcessedToDeviceEvent::UnableToDecrypt { event } => event.clone(),
ProcessedToDeviceEvent::PlainText(event) => event.clone(),
ProcessedToDeviceEvent::NotProcessed(event) => event.clone(),
}
}
}
/// The location of an event bundled in an `unsigned` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum UnsignedEventLocation {

View File

@@ -20,6 +20,12 @@ All notable changes to this project will be documented in this file.
### Features
- [**breaking**] `OlmMachine.receive_sync_changes` returns now a list of `ProcessedToDeviceEvent`
instead of a list of `Raw<AnyToDeviceEvent>`. With variants like `Decrypted`|`UnableToDecrypt`|`PlainText`|`NotProcessed`.
This allows for example to make the difference between an event sent in clear and an event successfully decrypted.
A `ProcessedToDeviceEvent::Decrypted` now contains the `encryption_info` and `verification_state` of the event.
For quick compatibility a helper `ProcessedToDeviceEvent::to_raw` allows to map back to the previous behaviour.
- [**breaking**] Add support for the shared history flag defined in
[MSC3061](https://github.com/matrix-org/matrix-spec-proposals/pull/3061).
The shared history flag is now respected when room keys are received as an

View File

@@ -21,9 +21,9 @@ use std::{
use itertools::Itertools;
use matrix_sdk_common::{
deserialized_responses::{
AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, UnableToDecryptInfo,
UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel,
VerificationState,
AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo,
ProcessedToDeviceEvent, UnableToDecryptInfo, UnableToDecryptReason,
UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState,
},
locks::RwLock as StdRwLock,
BoxFuture,
@@ -1286,7 +1286,7 @@ impl OlmMachine {
transaction: &mut StoreTransaction,
changes: &mut Changes,
mut raw_event: Raw<AnyToDeviceEvent>,
) -> Option<Raw<AnyToDeviceEvent>> {
) -> Option<ProcessedToDeviceEvent> {
Self::record_message_id(&raw_event);
let event: ToDeviceEvents = match raw_event.deserialize_as() {
@@ -1294,8 +1294,7 @@ impl OlmMachine {
Err(e) => {
// Skip invalid events.
warn!("Received an invalid to-device event: {e}");
return Some(raw_event);
return Some(ProcessedToDeviceEvent::NotProcessed(raw_event));
}
};
@@ -1320,7 +1319,7 @@ impl OlmMachine {
}
}
return Some(raw_event);
return Some(ProcessedToDeviceEvent::UnableToDecrypt { event: raw_event });
}
};
@@ -1372,12 +1371,75 @@ impl OlmMachine {
raw_event = decrypted.result.raw_event;
}
}
let encryption_info =
self.get_olm_encryption_info(&e.sender, decrypted.result.sender_key).await;
Some(ProcessedToDeviceEvent::Decrypted {
decrypted_event: raw_event,
encryption_info,
})
}
e => self.handle_to_device_event(changes, &e).await,
e => {
self.handle_to_device_event(changes, &e).await;
Some(ProcessedToDeviceEvent::PlainText(raw_event))
}
}
}
Some(raw_event)
/// Get the sender information for a successfully decrypted olm message.
///
/// # Arguments
///
/// * `sender` - The claimed user_id retrieved from the event.
///
/// * `sender_key` - The `Curve25519PublicKey` linked to the olm session
/// that decrypted the message.
///
/// # Returns
///
/// A [`EncryptionInfo`] struct.
async fn get_olm_encryption_info(
&self,
sender: &UserId,
sender_key: Curve25519PublicKey,
) -> EncryptionInfo {
let device =
self.store().get_device_from_curve_key(sender, sender_key).await.unwrap_or(None);
let state = if let Some(device) = &device {
if device.is_cross_signed_by_owner() {
if device.is_device_owner_verified() {
VerificationState::Verified
} else {
let identity = device
.device_owner_identity
.as_ref()
.expect("This device is cross-signed, so the identity exists");
if identity.was_previously_verified() {
VerificationState::Unverified(VerificationLevel::VerificationViolation)
} else {
VerificationState::Unverified(VerificationLevel::UnverifiedIdentity)
}
}
} else {
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
}
} else {
VerificationState::Unverified(VerificationLevel::None(DeviceLinkProblem::MissingDevice))
};
EncryptionInfo {
sender: sender.to_owned(),
sender_device: device.map(|d| d.device_id().to_owned()),
algorithm_info: AlgorithmInfo::OlmV1Curve25519AesSha2 {
curve25519_key: sender_key.to_base64(),
},
verification_state: state,
// Only relevant for megolm
session_id: None,
}
}
/// Decide whether a decrypted to-device event was sent from a dehydrated
@@ -1435,7 +1497,7 @@ impl OlmMachine {
pub async fn receive_sync_changes(
&self,
sync_changes: EncryptionSyncChanges<'_>,
) -> OlmResult<(Vec<Raw<AnyToDeviceEvent>>, Vec<RoomKeyInfo>)> {
) -> OlmResult<(Vec<ProcessedToDeviceEvent>, Vec<RoomKeyInfo>)> {
let mut store_transaction = self.inner.store.transaction().await;
let (events, changes) =
@@ -1464,10 +1526,18 @@ impl OlmMachine {
&self,
transaction: &mut StoreTransaction,
sync_changes: EncryptionSyncChanges<'_>,
) -> OlmResult<(Vec<Raw<AnyToDeviceEvent>>, Changes)> {
) -> OlmResult<(Vec<ProcessedToDeviceEvent>, Changes)> {
// Remove verification objects that have expired or are done.
let mut events = self.inner.verification_machine.garbage_collect();
let mut events: Vec<ProcessedToDeviceEvent> = self
.inner
.verification_machine
.garbage_collect()
.iter()
// These are `fake` to device events just serving as local echo
// in order for own client to react quickly to cancelled transaction.
// Just use PlainText for that.
.map(|e| ProcessedToDeviceEvent::PlainText(e.clone()))
.collect();
// The account is automatically saved by the store transaction created by the
// caller.
let mut changes = Default::default();

View File

@@ -17,8 +17,9 @@ use std::{fmt::Debug, iter, pin::Pin};
use assert_matches::assert_matches;
use futures_core::Stream;
use futures_util::{FutureExt, StreamExt};
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_test::async_test;
use ruma::{events::AnyToDeviceEvent, room_id, serde::Raw, user_id, RoomId, TransactionId, UserId};
use ruma::{room_id, user_id, RoomId, TransactionId, UserId};
use serde::Serialize;
use serde_json::json;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
@@ -286,7 +287,7 @@ async fn create_and_share_session_without_sender_data(
pub async fn receive_to_device_event<C>(
machine: &OlmMachine,
event: &ToDeviceEvent<C>,
) -> (Vec<Raw<AnyToDeviceEvent>>, Vec<RoomKeyInfo>)
) -> (Vec<ProcessedToDeviceEvent>, Vec<RoomKeyInfo>)
where
C: EventType + Serialize + Debug,
{

View File

@@ -18,8 +18,9 @@ use assert_matches2::{assert_let, assert_matches};
use futures_util::{pin_mut, FutureExt, StreamExt};
use itertools::Itertools;
use matrix_sdk_common::deserialized_responses::{
AlgorithmInfo, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
UnsignedEventLocation, VerificationLevel, VerificationState, WithheldCode,
AlgorithmInfo, ProcessedToDeviceEvent, UnableToDecryptInfo, UnableToDecryptReason,
UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState,
WithheldCode,
};
use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json};
use ruma::{
@@ -397,7 +398,7 @@ async fn test_room_key_sharing() {
let (decrypted, room_key_updates) =
send_room_key_to_device(&alice, &bob, room_id).await.unwrap();
let event = decrypted[0].deserialize().unwrap();
let event = decrypted[0].to_raw().deserialize().unwrap();
if let AnyToDeviceEvent::RoomKey(event) = event {
assert_eq!(&event.sender, alice.user_id());
@@ -489,7 +490,7 @@ async fn send_room_key_to_device(
sender: &OlmMachine,
receiver: &OlmMachine,
room_id: &RoomId,
) -> OlmResult<(Vec<Raw<AnyToDeviceEvent>>, Vec<RoomKeyInfo>)> {
) -> OlmResult<(Vec<ProcessedToDeviceEvent>, Vec<RoomKeyInfo>)> {
let to_device_requests = sender
.share_room_key(room_id, iter::once(receiver.user_id()), EncryptionSettings::default())
.await

View File

@@ -18,6 +18,7 @@ use std::{
};
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_test::async_test;
use ruma::{
device_id,
@@ -292,7 +293,11 @@ async fn test_decrypt_to_device_message_with_unsigned_sender_keys() {
// Bob receives the to-device message
let (to_device_events, _) = receive_to_device_event(&bob, &event).await;
let event = to_device_events.first().expect("Bob did not get a to-device event").clone();
// The to-device event should remain decrypted.
let event = to_device_events.first().expect("Bob did not get a to-device event");
let ProcessedToDeviceEvent::UnableToDecrypt { event } = event else {
panic!("Should refuse to decrypt")
};
assert_eq!(event.get_field("type").unwrap(), Some("m.room.encrypted"));
}

View File

@@ -13,8 +13,11 @@
// limitations under the License.
use assert_matches2::assert_matches;
use matrix_sdk_common::deserialized_responses::{
AlgorithmInfo, ProcessedToDeviceEvent, VerificationLevel, VerificationState,
};
use matrix_sdk_test::async_test;
use ruma::to_device::DeviceIdOrAllDevices;
use ruma::{events::AnyToDeviceEvent, serde::Raw, to_device::DeviceIdOrAllDevices};
use serde_json::{json, value::to_raw_value};
use crate::{
@@ -22,7 +25,10 @@ use crate::{
test_helpers::{get_machine_pair, get_machine_pair_with_session},
tests,
},
types::{events::ToDeviceEvent, requests::ToDeviceRequest},
types::{
events::{ToDeviceCustomEvent, ToDeviceEvent},
requests::ToDeviceRequest,
},
utilities::json_convert,
EncryptionSyncChanges, OlmError,
};
@@ -81,12 +87,15 @@ async fn test_send_encrypted_to_device() {
let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap();
assert_eq!(1, decrypted.len());
let decrypted_event = decrypted[0].deserialize().unwrap();
let processed_event = &decrypted[0];
let ProcessedToDeviceEvent::Decrypted { decrypted_event, .. } = processed_event else {
panic!("Unexpected variant");
};
let decrypted_event = decrypted_event.deserialize().unwrap();
assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned());
let decrypted_value = to_raw_value(&decrypted[0]).unwrap();
let decrypted_value = to_raw_value(&decrypted[0].to_raw()).unwrap();
let decrypted_value = serde_json::to_value(decrypted_value).unwrap();
assert_eq!(
@@ -100,6 +109,142 @@ async fn test_send_encrypted_to_device() {
);
}
#[async_test]
async fn test_processed_to_device_variants() {
let (alice, bob) =
get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;
let custom_event_type = "m.new_device";
let custom_content = json!({
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
});
let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
let raw_encrypted = device
.encrypt_event_raw(custom_event_type, &custom_content)
.await
.expect("Should have encryted the content");
let request = ToDeviceRequest::new(
bob.user_id(),
DeviceIdOrAllDevices::DeviceId(tests::bob_device_id().to_owned()),
"m.room.encrypted",
raw_encrypted.cast(),
);
let encrypted_event = ToDeviceEvent::new(
alice.user_id().to_owned(),
tests::to_device_requests_to_content(vec![request.clone().into()]),
);
let custom_event = json!({
"sender": "@alice:example.com",
"type": "m.new_device",
"content": {
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
}
});
let clear_event = serde_json::from_value::<ToDeviceCustomEvent>(custom_event.clone()).unwrap();
let encrypted_event = json_convert(&encrypted_event).unwrap();
let clear_event = json_convert(&clear_event).unwrap();
let malformed_event_no_type = json!({
"sender": "@alice:example.com",
"content": {
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
}
});
let malformed_event: Raw<AnyToDeviceEvent> =
serde_json::from_value(malformed_event_no_type).unwrap();
let alice_curve = alice
.get_device(alice.user_id(), alice.device_id(), None)
.await
.unwrap()
.unwrap()
.curve25519_key()
.unwrap();
let bob_curve = bob
.get_device(bob.user_id(), bob.device_id(), None)
.await
.unwrap()
.unwrap()
.curve25519_key()
.unwrap();
// let's add a UTD event
let utd_event = json!({
"content": {
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"ciphertext": {
bob_curve.to_base64(): {
// this payload is just captured from a sync of some other element web with other users
"body": "Awogjvpx458CGhuo77HX/+tp1sxgRoCi7iAlzMvfrpbWoREQAiKACysX/p+ojr5QitCi9WRXNyamW2v2LTvoyWKtVaA2oHnYGR5s5RYhDfnIgh5MMSqqKlAbfqLvrbLovTYcKagCBbFnbA43f6zYM44buGgy8q70hMVH5WP6aK1E9Z3DVZ+8PnXQGpsrxvz2IsL6w0Nzl/qUyBEQFcgkjoDPawbsZRCllMgq2LQUyqlun6IgDTCozqsfxhDWpdfYGde4z16m34Ang7f5pH+BmPrFs6E1AO5+UbhhhS6NwWlfEtA6/9yfMxWLz1d2OrLh+QG7lYFAU9/CzIoPxaHKKr4JxgL9CjsmUPyDymWOWHP0jLi1NwpOv6hGpx0FgM7jJIMk6gWGgC5rEgEeTIwdrJh3F9OKTNSva5hvD9LomGk6tZgzQG6oap1e3wiOUyTt6S7BlyMppIu3RlIiNihZ9e17JEGiGDXOXzMJ6ISAgvGVgTP7+EvyEt2Wt4du7uBo/UvljRvVNu3I8tfItizPAOlvz460+aBDxk+sflJWt7OnhiyPnOCfopb+1RzqKVCnnPyVaP2f4BPf8qpn/f5YZk+5jJgBrGPiHzzmb3sQ5pC470s6+U3MpVFlFTG/xPBtMRMwPsbKoHfnRPqIqGu5dQ1Sw7T6taDXWjP450TvjxgHK5t2z1rLA2SXzAB1P8xbi6YXqQwxL6PvMNHn/TM0jiIQHYuqg5/RKLyhHybfP8JAjgNBw9z16wfKR/YoYFr7c+S4McQaMNa8v2SxGzhpCC3duAoK2qCWLEkYRO5cMCsGm/9bf8Q+//OykygBU/hdkT1eHUbexgALPLdfhzduutU7pbChg4T7SH7euh/3NLmS/SQvkmPfm3ckbh/Vlcj9CsXws/7MX/VJbhpbyzgBNtMnbG6tAeAofMa6Go/yMgiNBZIhLpAm31iUbUhaGm2IIlF/lsmSYEiBPoSVfFU44tetX2I/PBDGiBlzyU+yC2TOEBwMGxBE3WHbIe5/7sKW8xJF9t+HBfxIyW1QRtY3EKdEcuVOTyMxYzq3L5OKOOtPDHObYiiXg00mAgdQqgfkEAIfoRCOa2NYfTedwwo0S77eQ1sPvW5Hhf+Cm+bLibkWzaYHEZF+vyE9/Tn0tZGtH07RXfUyhp1vtTH49OBZHGkb/r+L8OjYJTST1dDCGqeGXO3uwYjoWHXtezLVHYgL+UOwcLJfMF5s9DQiqcfYXzp2kEWGsaetBFXcUWqq4RMHqlr6QfbxyuYLlQzc/AYA/MrT3J6nDpNLcvozH3RcIs8NcKcjdtjvgL0QGThy3RcecJQEDx3STrkkePL3dlyFCtVsmtQ0vjBBCxUgdySfxiobGGnpezZYi7q+Xz61GOZ9QqYmkcZOPzfNWeqtmzB7gqlH1gkFsK2yMAzKq2XCDFHvA7YAT3yMGiY06FcQ+2jyg7Bk2Q+AvjTG8hlPlmt6BZfW5cz1qx1apQn1qHXHrgfWcI52rApYQlNPOU1Uc8kZ8Ee6XUhhXBGY1rvZiKjKFG0PPuS8xo4/P7/u+gH5gItmEVDFL6giYPFsPpqAQkUN7hFoGiVZEjO4PwrLOmydsEcNOfACqrnUs08FQtvPg0sjHnxh6nh6FUQv93ukKl6+c9d+pCsN2xukrQ7Dog3nrjFZ6PrS5J0k9rDAOwTB55sfGXPZ2rATOK1WS4XcpsCtqwnYm4sGNc8ALMQkQ97zCnw8TcQwLvdUMlfbqQ5ykDQpQD68fITEDDHmBAeTCjpC713E6AhvOMwTJvjhd7hSkeOTRTmn9zXIVGNo1jSr8u0xO9uLGeWsV0+UlRLgp7/nsgfermjwNN8wj6MW3DHGS8UzzYfe9TGCeywqqIUTqgfXY48leGgB7twh4cl4jcOQniLATTvigIAQIvq/Uv8L45BGnkpKTdQ5F73gehXdVA",
"type": 1
}
},
"org.matrix.msgid": "93ee0170aa7740d0ac9ee137e820302d",
"sender_key": alice_curve.to_base64(),
},
"type": "m.room.encrypted",
"sender": "@alice:example.org",
});
let utd_event: Raw<AnyToDeviceEvent> = serde_json::from_value(utd_event).unwrap();
let sync_changes = EncryptionSyncChanges {
to_device_events: vec![encrypted_event, clear_event, malformed_event, utd_event],
changed_devices: &Default::default(),
one_time_keys_counts: &Default::default(),
unused_fallback_keys: None,
next_batch_token: None,
};
let (processed, _) = bob.receive_sync_changes(sync_changes).await.unwrap();
assert_eq!(4, processed.len());
let processed_event = &processed[0];
let ProcessedToDeviceEvent::Decrypted { encryption_info, .. } = processed_event else {
panic!("Unexpected variant");
};
assert_eq!(alice.user_id().to_owned(), encryption_info.sender);
assert_eq!(Some(alice.device_id().to_owned()), encryption_info.sender_device);
let AlgorithmInfo::OlmV1Curve25519AesSha2 { curve25519_key } = &encryption_info.algorithm_info
else {
panic!("Unexpected algorithm info");
};
assert_eq!(curve25519_key.to_owned(), alice_curve.to_base64());
assert_eq!(encryption_info.session_id, None);
assert_eq!(
encryption_info.verification_state,
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
);
let processed_event = &processed[1];
let ProcessedToDeviceEvent::PlainText(_) = processed_event else {
panic!("Unexpected variant");
};
let processed_event = &processed[2];
let ProcessedToDeviceEvent::NotProcessed(_) = processed_event else {
panic!("Unexpected variant");
};
let processed_event = &processed[3];
let ProcessedToDeviceEvent::UnableToDecrypt { .. } = processed_event else {
panic!("Unexpected variant");
};
}
#[async_test]
async fn test_send_encrypted_to_device_no_session() {
let (alice, bob, _) = get_machine_pair(tests::alice_id(), tests::user_id(), false).await;

View File

@@ -1031,7 +1031,7 @@ mod tests {
};
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::WithheldCode;
use matrix_sdk_common::deserialized_responses::{ProcessedToDeviceEvent, WithheldCode};
use matrix_sdk_test::{async_test, ruma_response_from_json};
use ruma::{
api::client::{
@@ -1822,8 +1822,12 @@ mod tests {
let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap();
assert_eq!(1, decrypted.len());
use crate::types::events::EventType;
assert_let!(
ProcessedToDeviceEvent::Decrypted { decrypted_event, .. } =
decrypted.first().unwrap().clone()
);
assert_eq!(
decrypted[0].get_field::<String>("type").unwrap().unwrap(),
decrypted_event.get_field::<String>("type").unwrap().unwrap(),
RoomKeyBundleContent::EVENT_TYPE,
);
}