mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-07 15:33:45 -04:00
crypto: Support for encrypting and sending room key history bundle data
For each device belonging to the user, encrypt and send to-device messages containing the bundle data
This commit is contained in:
@@ -85,6 +85,7 @@ use crate::{
|
||||
RoomEventEncryptionScheme, SupportedEventEncryptionSchemes,
|
||||
},
|
||||
room_key::{MegolmV1AesSha2Content, RoomKeyContent},
|
||||
room_key_bundle::RoomKeyBundleContent,
|
||||
room_key_withheld::{
|
||||
MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, RoomKeyWithheldEvent,
|
||||
},
|
||||
@@ -98,8 +99,8 @@ use crate::{
|
||||
},
|
||||
utilities::timestamp_to_iso8601,
|
||||
verification::{Verification, VerificationMachine, VerificationRequest},
|
||||
CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, LocalTrust,
|
||||
RoomEventDecryptionResult, SignatureError, TrustRequirement,
|
||||
CollectStrategy, CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData,
|
||||
LocalTrust, RoomEventDecryptionResult, SignatureError, TrustRequirement,
|
||||
};
|
||||
|
||||
/// State machine implementation of the Olm/Megolm encryption protocol used for
|
||||
@@ -1089,6 +1090,22 @@ impl OlmMachine {
|
||||
self.inner.group_session_manager.share_room_key(room_id, users, encryption_settings).await
|
||||
}
|
||||
|
||||
/// Collect the devices belonging to the given user, and send the details of
|
||||
/// a room key bundle to those devices.
|
||||
///
|
||||
/// Returns a list of to-device requests which must be sent.
|
||||
pub async fn share_room_key_bundle_data(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
collect_strategy: &CollectStrategy,
|
||||
bundle_data: RoomKeyBundleContent,
|
||||
) -> OlmResult<Vec<ToDeviceRequest>> {
|
||||
self.inner
|
||||
.group_session_manager
|
||||
.share_room_key_bundle_data(user_id, collect_strategy, bundle_data)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Receive an unencrypted verification event.
|
||||
///
|
||||
/// This method can be used to pass verification events that are happening
|
||||
|
||||
@@ -17,6 +17,8 @@ mod share_strategy;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt::Debug,
|
||||
iter,
|
||||
iter::zip,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -29,11 +31,13 @@ use ruma::{
|
||||
events::{AnyMessageLikeEventContent, AnyToDeviceEventContent, ToDeviceEventType},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId,
|
||||
UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
pub(crate) use share_strategy::CollectRecipientsResult;
|
||||
pub use share_strategy::CollectStrategy;
|
||||
use tracing::{debug, error, info, instrument, trace};
|
||||
use tracing::{debug, error, info, instrument, trace, warn, Instrument};
|
||||
|
||||
use crate::{
|
||||
error::{EventError, MegolmResult, OlmResult},
|
||||
@@ -43,7 +47,14 @@ use crate::{
|
||||
ShareInfo, ShareState,
|
||||
},
|
||||
store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store},
|
||||
types::{events::room::encrypted::RoomEncryptedEventContent, requests::ToDeviceRequest},
|
||||
types::{
|
||||
events::{
|
||||
room::encrypted::{RoomEncryptedEventContent, ToDeviceEncryptedEventContent},
|
||||
room_key_bundle::RoomKeyBundleContent,
|
||||
EventType,
|
||||
},
|
||||
requests::ToDeviceRequest,
|
||||
},
|
||||
Device, DeviceData, EncryptionSettings, OlmError,
|
||||
};
|
||||
|
||||
@@ -254,8 +265,12 @@ impl GroupSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt the given content for the given devices and create to-device
|
||||
/// requests that sends the encrypted content to them.
|
||||
/// Encrypt the given group session key for the given devices and create
|
||||
/// to-device requests that sends the encrypted content to them.
|
||||
///
|
||||
/// See also [`encrypt_content_for_devices`] which is similar
|
||||
/// but is not specific to group sessions, and does not return the
|
||||
/// [`ShareInfo`] data.
|
||||
async fn encrypt_session_for(
|
||||
store: Arc<CryptoStoreWrapper>,
|
||||
group_session: OutboundGroupSession,
|
||||
@@ -408,11 +423,7 @@ impl GroupSessionManager {
|
||||
) -> OlmResult<Vec<(DeviceData, WithheldCode)>> {
|
||||
// If we have some recipients, log them here.
|
||||
if !recipient_devices.is_empty() {
|
||||
#[allow(unknown_lints, clippy::unwrap_or_default)] // false positive
|
||||
let recipients = recipient_devices.iter().fold(BTreeMap::new(), |mut acc, d| {
|
||||
acc.entry(d.user_id()).or_insert_with(BTreeSet::new).insert(d.device_id());
|
||||
acc
|
||||
});
|
||||
let recipients = recipient_list_to_users_and_devices(&recipient_devices);
|
||||
|
||||
// If there are new recipients we need to persist the outbound group
|
||||
// session as the to-device requests are persisted with the session.
|
||||
@@ -746,9 +757,179 @@ impl GroupSessionManager {
|
||||
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
/// Collect the devices belonging to the given user, and send the details of
|
||||
/// a room key bundle to those devices.
|
||||
///
|
||||
/// Returns a list of to-device requests which must be sent.
|
||||
///
|
||||
/// For security reasons, only "safe" [`CollectStrategy`]s are supported, in
|
||||
/// which the recipient must have signed their
|
||||
/// devices. [`CollectStrategy::AllDevices`] and
|
||||
/// [`CollectStrategy::ErrorOnVerifiedUserProblem`] are "unsafe" in this
|
||||
/// respect,and are treated the same as
|
||||
/// [`CollectStrategy::IdentityBasedStrategy`].
|
||||
#[instrument(skip(self, bundle_data))]
|
||||
pub async fn share_room_key_bundle_data(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
collect_strategy: &CollectStrategy,
|
||||
bundle_data: RoomKeyBundleContent,
|
||||
) -> OlmResult<Vec<ToDeviceRequest>> {
|
||||
// Only allow conservative sharing strategies
|
||||
let collect_strategy = match collect_strategy {
|
||||
CollectStrategy::AllDevices | CollectStrategy::ErrorOnVerifiedUserProblem => {
|
||||
warn!("Ignoring request to use unsafe sharing strategy {:?} for room key history sharing", collect_strategy);
|
||||
&CollectStrategy::IdentityBasedStrategy
|
||||
}
|
||||
CollectStrategy::IdentityBasedStrategy | CollectStrategy::OnlyTrustedDevices => {
|
||||
collect_strategy
|
||||
}
|
||||
};
|
||||
|
||||
let mut changes = Changes::default();
|
||||
|
||||
let CollectRecipientsResult { devices, .. } =
|
||||
share_strategy::collect_recipients_for_share_strategy(
|
||||
&self.store,
|
||||
iter::once(user_id),
|
||||
collect_strategy,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let devices = devices.into_values().flatten().collect();
|
||||
let event_type = bundle_data.event_type().to_owned();
|
||||
let (requests, _) = self
|
||||
.encrypt_content_for_devices(devices, &event_type, bundle_data, &mut changes)
|
||||
.await?;
|
||||
|
||||
// TODO: figure out what to do with withheld devices
|
||||
|
||||
// Persist any changes we might have collected.
|
||||
if !changes.is_empty() {
|
||||
let session_count = changes.sessions.len();
|
||||
|
||||
self.store.save_changes(changes).await?;
|
||||
|
||||
trace!(
|
||||
session_count = session_count,
|
||||
"Stored the changed sessions after encrypting an room key"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(requests)
|
||||
}
|
||||
|
||||
/// Encrypt the given content for the given devices and build to-device
|
||||
/// requests to send the encrypted content to them.
|
||||
///
|
||||
/// Returns a tuple containing (1) the list of to-device requests, and (2)
|
||||
/// the list of devices that we could not find an olm session for (so
|
||||
/// need a withheld message).
|
||||
async fn encrypt_content_for_devices(
|
||||
&self,
|
||||
recipient_devices: Vec<DeviceData>,
|
||||
event_type: &str,
|
||||
content: impl Serialize + Clone + Send + 'static,
|
||||
changes: &mut Changes,
|
||||
) -> OlmResult<(Vec<ToDeviceRequest>, Vec<(DeviceData, WithheldCode)>)> {
|
||||
let recipients = recipient_list_to_users_and_devices(&recipient_devices);
|
||||
info!(?recipients, "Encrypting content of type {}", event_type);
|
||||
|
||||
// Chunk the recipients out so each to-device request will contain a
|
||||
// limited amount of to-device messages.
|
||||
//
|
||||
// Create concurrent tasks for each chunk of recipients.
|
||||
let tasks: Vec<_> = recipient_devices
|
||||
.chunks(Self::MAX_TO_DEVICE_MESSAGES)
|
||||
.map(|chunk| {
|
||||
spawn(
|
||||
encrypt_content_for_devices(
|
||||
self.store.crypto_store(),
|
||||
event_type.to_owned(),
|
||||
content.clone(),
|
||||
chunk.to_vec(),
|
||||
)
|
||||
.in_current_span(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut no_olm_devices = Vec::new();
|
||||
let mut to_device_requests = Vec::new();
|
||||
|
||||
// Wait for all the tasks to finish up and queue up the Olm session that
|
||||
// was used to encrypt the room key to be persisted again. This is
|
||||
// needed because each encryption step will mutate the Olm session,
|
||||
// ratcheting its state forward.
|
||||
for result in join_all(tasks).await {
|
||||
let result = result.expect("Encryption task panicked")?;
|
||||
if let Some(request) = result.to_device_request {
|
||||
to_device_requests.push(request);
|
||||
}
|
||||
changes.sessions.extend(result.updated_olm_sessions);
|
||||
no_olm_devices.extend(result.no_olm_devices);
|
||||
}
|
||||
|
||||
Ok((to_device_requests, no_olm_devices))
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of [`GroupSessionManager::encrypt_session_for`]
|
||||
/// Helper for [`GroupSessionManager::encrypt_content_for_devices`].
|
||||
///
|
||||
/// Encrypt the given content for the given devices and build a to-device
|
||||
/// request to send the encrypted content to them.
|
||||
///
|
||||
/// See also [`GroupSessionManager::encrypt_session_for`], which is similar
|
||||
/// but applies specifically to `m.room_key` messages that hold a megolm
|
||||
/// session key.
|
||||
async fn encrypt_content_for_devices(
|
||||
store: Arc<CryptoStoreWrapper>,
|
||||
event_type: String,
|
||||
content: impl Serialize + Clone + Send + 'static,
|
||||
devices: Vec<DeviceData>,
|
||||
) -> OlmResult<EncryptForDevicesResult> {
|
||||
let mut result_builder = EncryptForDevicesResultBuilder::default();
|
||||
|
||||
async fn encrypt(
|
||||
store: Arc<CryptoStoreWrapper>,
|
||||
device: DeviceData,
|
||||
event_type: String,
|
||||
bundle_data: impl Serialize,
|
||||
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
|
||||
device.encrypt(store.as_ref(), &event_type, bundle_data).await
|
||||
}
|
||||
|
||||
let tasks = devices.iter().map(|device| {
|
||||
spawn(
|
||||
encrypt(store.clone(), device.clone(), event_type.clone(), content.clone())
|
||||
.in_current_span(),
|
||||
)
|
||||
});
|
||||
|
||||
let results = join_all(tasks).await;
|
||||
|
||||
for (device, result) in zip(devices, results) {
|
||||
let encryption_result = result.expect("Encryption task panicked");
|
||||
|
||||
match encryption_result {
|
||||
Ok((used_session, message)) => {
|
||||
result_builder.on_successful_encryption(&device, used_session, message.cast());
|
||||
}
|
||||
Err(OlmError::MissingSession) => {
|
||||
// There is no established Olm session for this device
|
||||
result_builder.on_missing_session(device);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result_builder.into_result())
|
||||
}
|
||||
|
||||
/// Result of [`GroupSessionManager::encrypt_session_for`] and
|
||||
/// [`encrypt_content_for_devices`].
|
||||
#[derive(Debug)]
|
||||
struct EncryptForDevicesResult {
|
||||
/// The request to send the to-device messages containing the encrypted
|
||||
@@ -830,6 +1011,16 @@ impl EncryptForDevicesResultBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn recipient_list_to_users_and_devices(
|
||||
recipient_devices: &[DeviceData],
|
||||
) -> BTreeMap<&UserId, BTreeSet<&DeviceId>> {
|
||||
#[allow(unknown_lints, clippy::unwrap_or_default)] // false positive
|
||||
recipient_devices.iter().fold(BTreeMap::new(), |mut acc, d| {
|
||||
acc.entry(d.user_id()).or_insert_with(BTreeSet::new).insert(d.device_id());
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
@@ -848,21 +1039,27 @@ mod tests {
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
device_id,
|
||||
events::room::history_visibility::HistoryVisibility,
|
||||
room_id,
|
||||
events::room::{
|
||||
history_visibility::HistoryVisibility, EncryptedFileInit, JsonWebKey, JsonWebKeyInit,
|
||||
},
|
||||
owned_room_id, room_id,
|
||||
serde::Base64,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
user_id, DeviceId, OneTimeKeyAlgorithm, TransactionId, UInt, UserId,
|
||||
user_id, DeviceId, OneTimeKeyAlgorithm, OwnedMxcUri, TransactionId, UInt, UserId,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{
|
||||
identities::DeviceData,
|
||||
machine::EncryptionSyncChanges,
|
||||
machine::{
|
||||
test_helpers::get_machine_pair_with_setup_sessions_test_helper, EncryptionSyncChanges,
|
||||
},
|
||||
olm::{Account, SenderData},
|
||||
session_manager::{group_sessions::CollectRecipientsResult, CollectStrategy},
|
||||
types::{
|
||||
events::{
|
||||
room::encrypted::EncryptedToDeviceEvent,
|
||||
room_key_bundle::RoomKeyBundleContent,
|
||||
room_key_withheld::RoomKeyWithheldContent::{self, MegolmV1AesSha2},
|
||||
},
|
||||
requests::ToDeviceRequest,
|
||||
@@ -1555,4 +1752,79 @@ mod tests {
|
||||
assert_eq!(event_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_room_key_bundle_sharing() {
|
||||
let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper(
|
||||
user_id!("@alice:localhost"),
|
||||
user_id!("@bob:localhost"),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Alice trusts Bob's device
|
||||
let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
|
||||
device.set_local_trust(LocalTrust::Verified).await.unwrap();
|
||||
|
||||
let content = RoomKeyBundleContent {
|
||||
room_id: owned_room_id!("!room:id"),
|
||||
file: (EncryptedFileInit {
|
||||
url: OwnedMxcUri::from("test"),
|
||||
key: JsonWebKey::from(JsonWebKeyInit {
|
||||
kty: "oct".to_owned(),
|
||||
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
|
||||
alg: "A256CTR".to_owned(),
|
||||
#[allow(clippy::unnecessary_to_owned)]
|
||||
k: Base64::new(vec![0u8; 0]),
|
||||
ext: true,
|
||||
}),
|
||||
iv: Base64::new(vec![0u8; 0]),
|
||||
hashes: Default::default(),
|
||||
v: "".to_owned(),
|
||||
})
|
||||
.into(),
|
||||
};
|
||||
|
||||
let requests = alice
|
||||
.share_room_key_bundle_data(
|
||||
bob.user_id(),
|
||||
&CollectStrategy::OnlyTrustedDevices,
|
||||
content,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// There should be exactly one message
|
||||
let requests: Vec<_> =
|
||||
requests.iter().filter(|r| r.event_type == "m.room.encrypted".into()).collect();
|
||||
let message_count: usize = requests.iter().map(|r| r.message_count()).sum();
|
||||
assert_eq!(message_count, 1);
|
||||
|
||||
// Bob decrypts the message
|
||||
let bob_message = requests[0]
|
||||
.messages
|
||||
.get(bob.user_id())
|
||||
.unwrap()
|
||||
.get(&(bob.device_id().to_owned().into()))
|
||||
.unwrap();
|
||||
let to_device = EncryptedToDeviceEvent::new(
|
||||
alice.user_id().to_owned(),
|
||||
bob_message.cast_ref().deserialize().unwrap(),
|
||||
);
|
||||
|
||||
let sync_changes = EncryptionSyncChanges {
|
||||
to_device_events: vec![crate::utilities::json_convert(&to_device).unwrap()],
|
||||
changed_devices: &Default::default(),
|
||||
one_time_keys_counts: &Default::default(),
|
||||
unused_fallback_keys: None,
|
||||
next_batch_token: None,
|
||||
};
|
||||
let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap();
|
||||
assert_eq!(1, decrypted.len());
|
||||
use crate::types::events::EventType;
|
||||
assert_eq!(
|
||||
decrypted[0].get_field::<String>("type").unwrap().unwrap(),
|
||||
RoomKeyBundleContent::EVENT_TYPE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod forwarded_room_key;
|
||||
pub mod olm_v1;
|
||||
pub mod room;
|
||||
pub mod room_key;
|
||||
pub mod room_key_bundle;
|
||||
pub mod room_key_request;
|
||||
pub mod room_key_withheld;
|
||||
pub mod secret_send;
|
||||
|
||||
39
crates/matrix-sdk-crypto/src/types/events/room_key_bundle.rs
Normal file
39
crates/matrix-sdk-crypto/src/types/events/room_key_bundle.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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.
|
||||
|
||||
//! Types for `io.element.msc4268.room_key_bundle` to-device events, per
|
||||
//! [MSC4268].
|
||||
//!
|
||||
//! [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
|
||||
|
||||
use ruma::OwnedRoomId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::EventType;
|
||||
|
||||
/// The `io.element.msc4268.room_key_bundle` event content. See [MSC4268].
|
||||
///
|
||||
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RoomKeyBundleContent {
|
||||
/// The room that these keys are for
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The location and encryption info of the key bundle.
|
||||
pub file: ruma::events::room::EncryptedFile,
|
||||
}
|
||||
|
||||
impl EventType for RoomKeyBundleContent {
|
||||
const EVENT_TYPE: &'static str = "io.element.msc4268.room_key_bundle";
|
||||
}
|
||||
Reference in New Issue
Block a user