mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-12 10:00:50 -04:00
354 lines
16 KiB
Rust
354 lines
16 KiB
Rust
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::io::Write;
|
|
|
|
use assert_matches::assert_matches;
|
|
use eyeball_im::VectorDiff;
|
|
use matrix_sdk::{
|
|
assert_next_matches_with_timeout,
|
|
linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
|
|
test_utils::mocks::MatrixMockServer,
|
|
};
|
|
use matrix_sdk_test::{BOB, async_test, event_factory::EventFactory};
|
|
use matrix_sdk_ui::timeline::RoomExt;
|
|
use ruma::{
|
|
event_id,
|
|
events::room::encrypted::{
|
|
EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent,
|
|
},
|
|
room_id,
|
|
};
|
|
use stream_assert::assert_pending;
|
|
use tempfile::NamedTempFile;
|
|
|
|
/// The event cache can store Unable To Decrypt event
|
|
/// ([`TimelineEventKind::UnableToDecrypt`]). If such event is part of the
|
|
/// initial items of the `Timeline`, they are automatically decrypted if
|
|
/// possible.
|
|
///
|
|
/// [`TimelineEventKind::UnableToDecrypt`]: matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt
|
|
#[async_test]
|
|
async fn test_an_utd_from_the_event_cache_as_an_initial_item_is_decrypted() {
|
|
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
|
const SESSION_KEY: &[u8] = b"\
|
|
-----BEGIN MEGOLM SESSION DATA-----\n\
|
|
ASKcWoiAVUM97482UAi83Avce62hSLce7i5JhsqoF6xeAAAACqt2Cg3nyJPRWTTMXxXH7TXnkfdlmBXbQtq5\
|
|
bpHo3LRijcq2Gc6TXilESCmJN14pIsfKRJrWjZ0squ/XsoTFytuVLWwkNaW3QF6obeg2IoVtJXLMPdw3b2vO\
|
|
vgwGY3OMP0XafH13j1vcb6YLzvgLkZQLnYvd47hv3yK/9GmKS9tokuaQ7dCVYckYcIOS09EDTs70YdxUd5WG\
|
|
rQynATCLFP1p/NAGv70r9MK7Cy/mNpjD0r4qC7UEDIoi1kOWzHgnLo19wtvwsb8Fg8ATxcs3Wmtj8hIUYpDx\
|
|
ia4sM10zbytUuaPUAfCDf42IyxdmOnGe1CueXhgI71y+RW0s0argNqUt7jB70JT0o9CyX6UBGRaqLk2MPY9T\
|
|
hUu5J8X3UgIa6rcbWigzohzWm9rdbEHFrSWqjpfQYMaAKQQgETrjSy4XTrp2RhC2oNqG/hylI4ab+F4X6fpH\
|
|
DYP1NqNMP5g36xNu7LhDnrUB5qsPjYOmWORxGLfudpF3oLYCSlr3DgHqEIB6HjQblLZ3KQuPBse3zxyROTnS\
|
|
AhdPH4a/z1wioFtKNVph3hecsiKEdqnz4Y2coSIdhz58mJ9JWNQoFAENE5CSsoEZAGvafYZVpW4C75YY2zq1\
|
|
wIeiFi1dT43/jLAUGkslsi1VvnyfUu8qO404RxYO3XHoGLMFoFLOO+lZ+VGci2Vz10AhxJhEBHxRKxw4k2uB\
|
|
HztoSJUr/2Y\n\
|
|
-----END MEGOLM SESSION DATA-----";
|
|
|
|
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
|
|
let event_factory = EventFactory::new().room(room_id).sender(&BOB);
|
|
|
|
let mock_server = MatrixMockServer::new().await;
|
|
let client = mock_server.client_builder().build().await;
|
|
|
|
// Set up the event cache store.
|
|
{
|
|
let event_cache_store = client.event_cache_store().lock().await.unwrap();
|
|
|
|
// The event cache contains one chunk as such:
|
|
//
|
|
// 1. a chunk of 1 item
|
|
//
|
|
// The item is an encrypted event! It has been stored before having a chance to
|
|
// be decrypted. Damn. We want to see if decryption will trigger automatically.
|
|
event_cache_store
|
|
.handle_linked_chunk_updates(
|
|
LinkedChunkId::Room(room_id),
|
|
vec![
|
|
// chunk #1
|
|
Update::NewItemsChunk {
|
|
previous: None,
|
|
new: ChunkIdentifier::new(0),
|
|
next: None,
|
|
},
|
|
// … and its item
|
|
Update::PushItems {
|
|
at: Position::new(ChunkIdentifier::new(0), 0),
|
|
items: vec![event_factory
|
|
.event(RoomEncryptedEventContent::new(
|
|
EncryptedEventScheme::MegolmV1AesSha2(
|
|
MegolmV1AesSha2ContentInit {
|
|
ciphertext: "\
|
|
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
|
|
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
|
|
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
|
|
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
|
|
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
|
|
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
|
|
.to_owned(),
|
|
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA"
|
|
.to_owned(),
|
|
device_id: "NLAZCWIOCO".into(),
|
|
session_id: SESSION_ID.into(),
|
|
}
|
|
.into(),
|
|
),
|
|
None,
|
|
))
|
|
.event_id(event_id!("$ev0"))
|
|
.into_utd_sync_timeline_event(),
|
|
],
|
|
},
|
|
],
|
|
)
|
|
.await
|
|
.expect("Failed to setup the event cache");
|
|
}
|
|
|
|
// Import the key to decrypt the cached event.
|
|
{
|
|
let mut tempfile =
|
|
NamedTempFile::new().expect("Failed to create a temporary file for the keys");
|
|
tempfile.write_all(SESSION_KEY).expect("Failed to write the keys in the temporary file");
|
|
let tempfile_path = tempfile.into_temp_path();
|
|
|
|
let room_key_import_result = client
|
|
.encryption()
|
|
.import_room_keys(tempfile_path.to_path_buf(), "1234")
|
|
.await
|
|
.expect("Failed to import the keys");
|
|
assert_eq!(room_key_import_result.imported_count, 1);
|
|
}
|
|
|
|
// Set up the event cache.
|
|
let event_cache = client.event_cache();
|
|
event_cache.subscribe().unwrap();
|
|
|
|
let room = mock_server.sync_joined_room(&client, room_id).await;
|
|
let timeline = room.timeline().await.unwrap();
|
|
let (initial_updates, mut updates_stream) = timeline.subscribe().await;
|
|
|
|
assert_eq!(initial_updates.len(), 2);
|
|
|
|
// First item is the date divider.
|
|
assert!(&initial_updates[0].is_date_divider());
|
|
|
|
// Second item is `$ev0` but as an UTD!
|
|
assert_matches!(&initial_updates[1].as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert!(event.content().is_unable_to_decrypt());
|
|
});
|
|
|
|
// But wait… there is more!
|
|
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
|
|
// Ho ho, one update?
|
|
assert_eq!(updates.len(), 1, "Expecting 1 update from the `Timeline`");
|
|
|
|
// Yes, the event is decrypted!
|
|
assert_matches!(&updates[0], VectorDiff::Set { index: 1, value: event } => {
|
|
assert_matches!(event.as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert_matches!(event.content().as_message(), Some(message) => {
|
|
assert_eq!(message.body(), "It's a secret to everybody");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// That's all folks!
|
|
assert_pending!(updates_stream);
|
|
}
|
|
|
|
/// The event cache can store Unable To Decrypt event
|
|
/// ([`TimelineEventKind::UnableToDecrypt`]). If such event is part of the
|
|
/// paginated items of the `Timeline`, they are automatically decrypted if
|
|
/// possible.
|
|
///
|
|
/// [`TimelineEventKind::UnableToDecrypt`]: matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt
|
|
#[async_test]
|
|
async fn test_an_utd_from_the_event_cache_as_a_paginated_item_is_decrypted() {
|
|
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
|
const SESSION_KEY: &[u8] = b"\
|
|
-----BEGIN MEGOLM SESSION DATA-----\n\
|
|
ASKcWoiAVUM97482UAi83Avce62hSLce7i5JhsqoF6xeAAAACqt2Cg3nyJPRWTTMXxXH7TXnkfdlmBXbQtq5\
|
|
bpHo3LRijcq2Gc6TXilESCmJN14pIsfKRJrWjZ0squ/XsoTFytuVLWwkNaW3QF6obeg2IoVtJXLMPdw3b2vO\
|
|
vgwGY3OMP0XafH13j1vcb6YLzvgLkZQLnYvd47hv3yK/9GmKS9tokuaQ7dCVYckYcIOS09EDTs70YdxUd5WG\
|
|
rQynATCLFP1p/NAGv70r9MK7Cy/mNpjD0r4qC7UEDIoi1kOWzHgnLo19wtvwsb8Fg8ATxcs3Wmtj8hIUYpDx\
|
|
ia4sM10zbytUuaPUAfCDf42IyxdmOnGe1CueXhgI71y+RW0s0argNqUt7jB70JT0o9CyX6UBGRaqLk2MPY9T\
|
|
hUu5J8X3UgIa6rcbWigzohzWm9rdbEHFrSWqjpfQYMaAKQQgETrjSy4XTrp2RhC2oNqG/hylI4ab+F4X6fpH\
|
|
DYP1NqNMP5g36xNu7LhDnrUB5qsPjYOmWORxGLfudpF3oLYCSlr3DgHqEIB6HjQblLZ3KQuPBse3zxyROTnS\
|
|
AhdPH4a/z1wioFtKNVph3hecsiKEdqnz4Y2coSIdhz58mJ9JWNQoFAENE5CSsoEZAGvafYZVpW4C75YY2zq1\
|
|
wIeiFi1dT43/jLAUGkslsi1VvnyfUu8qO404RxYO3XHoGLMFoFLOO+lZ+VGci2Vz10AhxJhEBHxRKxw4k2uB\
|
|
HztoSJUr/2Y\n\
|
|
-----END MEGOLM SESSION DATA-----";
|
|
|
|
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
|
|
let event_factory = EventFactory::new().room(room_id).sender(&BOB);
|
|
|
|
let mock_server = MatrixMockServer::new().await;
|
|
let client = mock_server.client_builder().build().await;
|
|
|
|
// Set up the event cache store.
|
|
{
|
|
let event_cache_store = client.event_cache_store().lock().await.unwrap();
|
|
|
|
// The event cache contains one chunk as such:
|
|
//
|
|
// 1. a chunk of 1 item
|
|
// 2. a chunk of 1 item
|
|
//
|
|
// The older item is an encrypted event! It has been stored before having a
|
|
// chance to be decrypted. Damn. We want to see if decryption will trigger
|
|
// automatically.
|
|
event_cache_store
|
|
.handle_linked_chunk_updates(
|
|
LinkedChunkId::Room(room_id),
|
|
vec![
|
|
// chunk #1
|
|
Update::NewItemsChunk {
|
|
previous: None,
|
|
new: ChunkIdentifier::new(0),
|
|
next: None,
|
|
},
|
|
// … and its item
|
|
Update::PushItems {
|
|
at: Position::new(ChunkIdentifier::new(0), 0),
|
|
items: vec![event_factory
|
|
.event(RoomEncryptedEventContent::new(
|
|
EncryptedEventScheme::MegolmV1AesSha2(
|
|
MegolmV1AesSha2ContentInit {
|
|
ciphertext: "\
|
|
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
|
|
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
|
|
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
|
|
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
|
|
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
|
|
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
|
|
.to_owned(),
|
|
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA"
|
|
.to_owned(),
|
|
device_id: "NLAZCWIOCO".into(),
|
|
session_id: SESSION_ID.into(),
|
|
}
|
|
.into(),
|
|
),
|
|
None,
|
|
))
|
|
.event_id(event_id!("$ev0"))
|
|
.into_utd_sync_timeline_event(),
|
|
],
|
|
},
|
|
// chunk #2
|
|
Update::NewItemsChunk {
|
|
previous: Some(ChunkIdentifier::new(0)),
|
|
new: ChunkIdentifier::new(1),
|
|
next: None,
|
|
},
|
|
// … and its item
|
|
Update::PushItems {
|
|
at: Position::new(ChunkIdentifier::new(1), 0),
|
|
items: vec![event_factory
|
|
.text_msg("hello")
|
|
.event_id(event_id!("$ev1"))
|
|
.into_event()
|
|
]
|
|
}
|
|
],
|
|
)
|
|
.await
|
|
.expect("Failed to setup the event cache");
|
|
}
|
|
|
|
// Import the key to decrypt the cached event.
|
|
{
|
|
let mut tempfile =
|
|
NamedTempFile::new().expect("Failed to create a temporary file for the keys");
|
|
tempfile.write_all(SESSION_KEY).expect("Failed to write the keys in the temporary file");
|
|
let tempfile_path = tempfile.into_temp_path();
|
|
|
|
let room_key_import_result = client
|
|
.encryption()
|
|
.import_room_keys(tempfile_path.to_path_buf(), "1234")
|
|
.await
|
|
.expect("Failed to import the keys");
|
|
assert_eq!(room_key_import_result.imported_count, 1);
|
|
}
|
|
|
|
// Set up the event cache.
|
|
let event_cache = client.event_cache();
|
|
event_cache.subscribe().unwrap();
|
|
|
|
let room = mock_server.sync_joined_room(&client, room_id).await;
|
|
let timeline = room.timeline().await.unwrap();
|
|
let (initial_updates, mut updates_stream) = timeline.subscribe().await;
|
|
|
|
assert_eq!(initial_updates.len(), 2);
|
|
|
|
// First item is the date divider.
|
|
assert!(&initial_updates[0].is_date_divider());
|
|
|
|
// Second item is `$ev1`.
|
|
assert_matches!(&initial_updates[1].as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev1");
|
|
assert_matches!(event.content().as_message(), Some(message) => {
|
|
assert_eq!(message.body(), "hello");
|
|
});
|
|
});
|
|
|
|
// The stream is pending, because, well, everything is alright so far!
|
|
assert_pending!(updates_stream);
|
|
|
|
// Now we can paginate to load the UTD!
|
|
let reached_start = timeline.paginate_backwards(1).await.unwrap();
|
|
|
|
// We have reached the start of the timeline. Not really part of this test, but
|
|
// let's test everything :-).
|
|
assert!(reached_start);
|
|
|
|
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
|
|
assert_eq!(updates.len(), 1, "We get the start of timeline item");
|
|
|
|
assert_matches!(&updates[0], VectorDiff::PushFront { value } => {
|
|
assert!(value.is_timeline_start());
|
|
});
|
|
});
|
|
|
|
// Now, let's look at the updates. We must observe an update reflecting the UTD
|
|
// has entered the `Timeline`.
|
|
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
|
|
assert_eq!(updates.len(), 2, "Expecting 2 updates from the `Timeline`");
|
|
|
|
// UTD! UTD!
|
|
assert_matches!(&updates[0], VectorDiff::Insert { index: 2, value: event } => {
|
|
assert_matches!(event.as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert!(event.content().is_unable_to_decrypt());
|
|
});
|
|
});
|
|
|
|
// UTD is decrypted now!
|
|
assert_matches!(&updates[1], VectorDiff::Set { index: 2, value: event } => {
|
|
assert_matches!(event.as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert_matches!(event.content().as_message(), Some(message) => {
|
|
assert_eq!(message.body(), "It's a secret to everybody");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// That's all folks!
|
|
assert_pending!(updates_stream);
|
|
}
|