Skip to content

Commit 35e13b8

Browse files
authored
Merge pull request #4903 from matrix-org/rav/history_sharing/upload_bundle_prep
crypto: prep work for sharing room key history bundles
2 parents f266fb9 + dc3bd69 commit 35e13b8

File tree

3 files changed

+170
-75
lines changed

3 files changed

+170
-75
lines changed

crates/matrix-sdk-crypto/src/identities/device.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::{
2121
},
2222
};
2323

24-
use matrix_sdk_common::{deserialized_responses::WithheldCode, locks::RwLock};
24+
use matrix_sdk_common::locks::RwLock;
2525
use ruma::{
2626
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
2727
events::{key::verification::VerificationMethod, AnyToDeviceEventContent},
@@ -64,9 +64,9 @@ pub enum MaybeEncryptedRoomKey {
6464
share_info: ShareInfo,
6565
message: Raw<AnyToDeviceEventContent>,
6666
},
67-
Withheld {
68-
code: WithheldCode,
69-
},
67+
/// We could not encrypt a message to this device because there is no active
68+
/// Olm session.
69+
MissingSession,
7070
}
7171

7272
/// A read-only version of a `Device`.
@@ -841,9 +841,7 @@ impl DeviceData {
841841
message: encrypted.cast(),
842842
}),
843843

844-
Err(OlmError::MissingSession) => {
845-
Ok(MaybeEncryptedRoomKey::Withheld { code: WithheldCode::NoOlm })
846-
}
844+
Err(OlmError::MissingSession) => Ok(MaybeEncryptedRoomKey::MissingSession),
847845
Err(e) => Err(e),
848846
}
849847
}

crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use matrix_sdk_common::{
2626
deserialized_responses::WithheldCode, executor::spawn, locks::RwLock as StdRwLock,
2727
};
2828
use ruma::{
29-
events::{AnyMessageLikeEventContent, ToDeviceEventType},
29+
events::{AnyMessageLikeEventContent, AnyToDeviceEventContent, ToDeviceEventType},
3030
serde::Raw,
3131
to_device::DeviceIdOrAllDevices,
3232
OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
@@ -254,29 +254,24 @@ impl GroupSessionManager {
254254
}
255255
}
256256

257-
/// Encrypt the given content for the given devices and create a to-device
257+
/// Encrypt the given content for the given devices and create to-device
258258
/// requests that sends the encrypted content to them.
259259
async fn encrypt_session_for(
260260
store: Arc<CryptoStoreWrapper>,
261261
group_session: OutboundGroupSession,
262262
devices: Vec<DeviceData>,
263263
) -> OlmResult<(
264-
OwnedTransactionId,
265-
ToDeviceRequest,
264+
EncryptForDevicesResult,
266265
BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, ShareInfo>>,
267-
Vec<Session>,
268-
Vec<(DeviceData, WithheldCode)>,
269266
)> {
270267
// Use a named type instead of a tuple with rather long type name
271268
pub struct DeviceResult {
272269
device: DeviceData,
273270
maybe_encrypted_room_key: MaybeEncryptedRoomKey,
274271
}
275272

276-
let mut messages = BTreeMap::new();
277-
let mut changed_sessions = Vec::new();
273+
let mut result_builder = EncryptForDevicesResultBuilder::default();
278274
let mut share_infos = BTreeMap::new();
279-
let mut withheld_devices = Vec::new();
280275

281276
// XXX is there a way to do this that doesn't involve cloning the
282277
// `Arc<CryptoStoreWrapper>` for each device?
@@ -300,35 +295,22 @@ impl GroupSessionManager {
300295

301296
match result.maybe_encrypted_room_key {
302297
MaybeEncryptedRoomKey::Encrypted { used_session, share_info, message } => {
303-
changed_sessions.push(used_session);
298+
result_builder.on_successful_encryption(&result.device, used_session, message);
304299

305300
let user_id = result.device.user_id().to_owned();
306301
let device_id = result.device.device_id().to_owned();
307-
308-
messages
309-
.entry(user_id.to_owned())
310-
.or_insert_with(BTreeMap::new)
311-
.insert(DeviceIdOrAllDevices::DeviceId(device_id.to_owned()), message);
312-
313302
share_infos
314303
.entry(user_id)
315304
.or_insert_with(BTreeMap::new)
316305
.insert(device_id, share_info);
317306
}
318-
MaybeEncryptedRoomKey::Withheld { code } => {
319-
withheld_devices.push((result.device, code));
307+
MaybeEncryptedRoomKey::MissingSession => {
308+
result_builder.on_missing_session(result.device);
320309
}
321310
}
322311
}
323312

324-
let txn_id = TransactionId::new();
325-
let request = ToDeviceRequest {
326-
event_type: ToDeviceEventType::RoomEncrypted,
327-
txn_id: txn_id.to_owned(),
328-
messages,
329-
};
330-
331-
Ok((txn_id, request, share_infos, changed_sessions, withheld_devices))
313+
Ok((result_builder.into_result(), share_infos))
332314
}
333315

334316
/// Given a list of user and an outbound session, return the list of users
@@ -353,21 +335,16 @@ impl GroupSessionManager {
353335
outbound: OutboundGroupSession,
354336
sessions: GroupSessionCache,
355337
) -> OlmResult<(Vec<Session>, Vec<(DeviceData, WithheldCode)>)> {
356-
let (id, request, share_infos, used_sessions, no_olm) =
338+
let (result, share_infos) =
357339
Self::encrypt_session_for(store, outbound.clone(), chunk).await?;
358340

359-
if !request.messages.is_empty() {
360-
trace!(
361-
recipient_count = request.message_count(),
362-
transaction_id = ?id,
363-
"Created a to-device request carrying a room_key"
364-
);
365-
341+
if let Some(request) = result.to_device_request {
342+
let id = request.txn_id.clone();
366343
outbound.add_request(id.clone(), request.into(), share_infos);
367344
sessions.mark_as_being_shared(id, outbound.clone());
368345
}
369346

370-
Ok((used_sessions, no_olm))
347+
Ok((result.updated_olm_sessions, result.no_olm_devices))
371348
}
372349

373350
pub(crate) fn session_cache(&self) -> GroupSessionCache {
@@ -771,6 +748,88 @@ impl GroupSessionManager {
771748
}
772749
}
773750

751+
/// Result of [`GroupSessionManager::encrypt_session_for`]
752+
#[derive(Debug)]
753+
struct EncryptForDevicesResult {
754+
/// The request to send the to-device messages containing the encrypted
755+
/// payload, if any devices were found.
756+
to_device_request: Option<ToDeviceRequest>,
757+
758+
/// The devices which lack an Olm session and therefore need a withheld code
759+
no_olm_devices: Vec<(DeviceData, WithheldCode)>,
760+
761+
/// The Olm sessions which were used to encrypt the requests and now need
762+
/// persisting to the store.
763+
updated_olm_sessions: Vec<Session>,
764+
}
765+
766+
/// A helper for building [`EncryptForDevicesResult`]
767+
#[derive(Debug, Default)]
768+
struct EncryptForDevicesResultBuilder {
769+
/// The payloads of the to-device messages
770+
messages: BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>,
771+
772+
/// The devices which lack an Olm session and therefore need a withheld code
773+
no_olm_devices: Vec<(DeviceData, WithheldCode)>,
774+
775+
/// The Olm sessions which were used to encrypt the requests and now need
776+
/// persisting to the store.
777+
updated_olm_sessions: Vec<Session>,
778+
}
779+
780+
impl EncryptForDevicesResultBuilder {
781+
/// Record a successful encryption. The encrypted message is added to the
782+
/// list to be sent, and the olm session is added to the list of those
783+
/// that have been modified.
784+
pub fn on_successful_encryption(
785+
&mut self,
786+
device: &DeviceData,
787+
used_session: Session,
788+
message: Raw<AnyToDeviceEventContent>,
789+
) {
790+
self.updated_olm_sessions.push(used_session);
791+
792+
self.messages
793+
.entry(device.user_id().to_owned())
794+
.or_default()
795+
.insert(DeviceIdOrAllDevices::DeviceId(device.device_id().to_owned()), message);
796+
}
797+
798+
/// Record a device which didn't have an active Olm session.
799+
pub fn on_missing_session(&mut self, device: DeviceData) {
800+
self.no_olm_devices.push((device, WithheldCode::NoOlm));
801+
}
802+
803+
/// Transform the accumulated results into an [`EncryptForDevicesResult`],
804+
/// wrapping the messages, if any, into a `ToDeviceRequest`.
805+
pub fn into_result(self) -> EncryptForDevicesResult {
806+
let EncryptForDevicesResultBuilder { updated_olm_sessions, no_olm_devices, messages } =
807+
self;
808+
809+
let mut encrypt_for_devices_result = EncryptForDevicesResult {
810+
to_device_request: None,
811+
updated_olm_sessions,
812+
no_olm_devices,
813+
};
814+
815+
if !messages.is_empty() {
816+
let request = ToDeviceRequest {
817+
event_type: ToDeviceEventType::RoomEncrypted,
818+
txn_id: TransactionId::new(),
819+
messages,
820+
};
821+
trace!(
822+
recipient_count = request.message_count(),
823+
transaction_id = ?request.txn_id,
824+
"Created a to-device request carrying room keys",
825+
);
826+
encrypt_for_devices_result.to_device_request = Some(request);
827+
};
828+
829+
encrypt_for_devices_result
830+
}
831+
}
832+
774833
#[cfg(test)]
775834
mod tests {
776835
use std::{

crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -156,23 +156,13 @@ pub(crate) async fn collect_session_recipients(
156156
settings: &EncryptionSettings,
157157
outbound: &OutboundGroupSession,
158158
) -> OlmResult<CollectRecipientsResult> {
159-
let users: BTreeSet<&UserId> = users.collect();
160-
let mut result = CollectRecipientsResult::default();
161-
let mut verified_users_with_new_identities: Vec<OwnedUserId> = Default::default();
162-
163-
trace!(?users, ?settings, "Calculating group session recipients");
164-
165-
let users_shared_with: BTreeSet<OwnedUserId> =
166-
outbound.shared_with_set.read().keys().cloned().collect();
167-
168-
let users_shared_with: BTreeSet<&UserId> = users_shared_with.iter().map(Deref::deref).collect();
169-
170-
// A user left if a user is missing from the set of users that should
171-
// get the session but is in the set of users that received the session.
172-
let user_left = !users_shared_with.difference(&users).collect::<BTreeSet<_>>().is_empty();
173-
174-
let visibility_changed = outbound.settings().history_visibility != settings.history_visibility;
175-
let algorithm_changed = outbound.settings().algorithm != settings.algorithm;
159+
let mut result = collect_recipients_for_share_strategy(
160+
store,
161+
users,
162+
&settings.sharing_strategy,
163+
Some(outbound),
164+
)
165+
.await?;
176166

177167
// To protect the room history we need to rotate the session if either:
178168
//
@@ -181,13 +171,65 @@ pub(crate) async fn collect_session_recipients(
181171
// 3. The history visibility changed.
182172
// 4. The encryption algorithm changed.
183173
//
184-
// This is calculated in the following code and stored in this variable.
185-
result.should_rotate = user_left || visibility_changed || algorithm_changed;
174+
// `result.should_rotate` is true if the first or second in that list is true;
175+
// we now need to check for the other two.
176+
let device_removed = result.should_rotate;
177+
178+
let visibility_changed = outbound.settings().history_visibility != settings.history_visibility;
179+
let algorithm_changed = outbound.settings().algorithm != settings.algorithm;
180+
181+
result.should_rotate = device_removed || visibility_changed || algorithm_changed;
182+
183+
if result.should_rotate {
184+
debug!(
185+
device_removed,
186+
visibility_changed, algorithm_changed, "Rotating room key to protect room history",
187+
);
188+
}
189+
190+
Ok(result)
191+
}
192+
193+
/// Given a list of users and a [`CollectStrategy`], return the list of devices
194+
/// that cryptographic keys should be shared with, or that withheld notices
195+
/// should be sent to.
196+
///
197+
/// If an existing [`OutboundGroupSession`] is provided, will also check the
198+
/// list of devices that the session has been *previously* shared with, and
199+
/// if that list is too broad, returns a flag indicating that the session should
200+
/// be rotated (e.g., because a device has been deleted or a user has left the
201+
/// chat).
202+
pub(crate) async fn collect_recipients_for_share_strategy(
203+
store: &Store,
204+
users: impl Iterator<Item = &UserId>,
205+
share_strategy: &CollectStrategy,
206+
outbound: Option<&OutboundGroupSession>,
207+
) -> OlmResult<CollectRecipientsResult> {
208+
let users: BTreeSet<&UserId> = users.collect();
209+
trace!(?users, ?share_strategy, "Calculating group session recipients");
210+
211+
let mut result = CollectRecipientsResult::default();
212+
let mut verified_users_with_new_identities: Vec<OwnedUserId> = Default::default();
213+
214+
// If we have an outbound session, check if a user is missing from the set of
215+
// users that should get the session but is in the set of users that
216+
// received the session.
217+
if let Some(outbound) = outbound {
218+
let users_shared_with: BTreeSet<OwnedUserId> =
219+
outbound.shared_with_set.read().keys().cloned().collect();
220+
let users_shared_with: BTreeSet<&UserId> =
221+
users_shared_with.iter().map(Deref::deref).collect();
222+
let left_users = users_shared_with.difference(&users).collect::<BTreeSet<_>>();
223+
if !left_users.is_empty() {
224+
trace!(?left_users, "Some users have left the chat: session must be rotated");
225+
result.should_rotate = true;
226+
}
227+
}
186228

187229
let own_identity = store.get_user_identity(store.user_id()).await?.and_then(|i| i.into_own());
188230

189231
// Get the recipient and withheld devices, based on the collection strategy.
190-
match settings.sharing_strategy {
232+
match share_strategy {
191233
CollectStrategy::AllDevices => {
192234
for user_id in users {
193235
trace!(
@@ -325,15 +367,6 @@ pub(crate) async fn collect_session_recipients(
325367
));
326368
}
327369

328-
if result.should_rotate {
329-
debug!(
330-
result.should_rotate,
331-
user_left,
332-
visibility_changed,
333-
algorithm_changed,
334-
"Rotating room key to protect room history",
335-
);
336-
}
337370
trace!(result.should_rotate, "Done calculating group session recipients");
338371

339372
Ok(result)
@@ -343,17 +376,22 @@ pub(crate) async fn collect_session_recipients(
343376
/// user.
344377
fn update_recipients_for_user(
345378
recipients: &mut CollectRecipientsResult,
346-
outbound: &OutboundGroupSession,
379+
outbound: Option<&OutboundGroupSession>,
347380
user_id: &UserId,
348381
recipient_devices: RecipientDevicesForUser,
349382
) {
350383
// If we haven't already concluded that the session should be
351384
// rotated for other reasons, we also need to check whether any
352385
// of the devices in the session got deleted or blacklisted in the
353386
// meantime. If so, we should also rotate the session.
354-
if !recipients.should_rotate {
355-
recipients.should_rotate =
356-
is_session_overshared_for_user(outbound, user_id, &recipient_devices.allowed_devices)
387+
if let Some(outbound) = outbound {
388+
if !recipients.should_rotate {
389+
recipients.should_rotate = is_session_overshared_for_user(
390+
outbound,
391+
user_id,
392+
&recipient_devices.allowed_devices,
393+
)
394+
}
357395
}
358396

359397
recipients

0 commit comments

Comments
 (0)