use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
use dashmap::DashMap;
use matrix_sdk_common::{
api::r0::to_device::DeviceIdOrAllDevices,
events::{room::encrypted::EncryptedEventContent, AnyMessageEventContent, EventType},
identifiers::{RoomId, UserId},
uuid::Uuid,
};
use tracing::{debug, info};
use crate::{
error::{EventError, MegolmResult, OlmResult},
olm::{Account, InboundGroupSession, OutboundGroupSession},
store::{Changes, Store},
Device, EncryptionSettings, OlmError, ToDeviceRequest,
};
#[derive(Debug, Clone)]
pub struct GroupSessionManager {
account: Account,
store: Store,
outbound_group_sessions: Arc<DashMap<RoomId, OutboundGroupSession>>,
outbound_sessions_being_shared: Arc<DashMap<Uuid, OutboundGroupSession>>,
}
impl GroupSessionManager {
const MAX_TO_DEVICE_MESSAGES: usize = 20;
pub(crate) fn new(account: Account, store: Store) -> Self {
Self {
account,
store,
outbound_group_sessions: Arc::new(DashMap::new()),
outbound_sessions_being_shared: Arc::new(DashMap::new()),
}
}
pub fn invalidate_group_session(&self, room_id: &RoomId) -> bool {
self.outbound_group_sessions.remove(room_id).is_some()
}
pub fn mark_request_as_sent(&self, request_id: &Uuid) {
if let Some((_, s)) = self.outbound_sessions_being_shared.remove(request_id) {
s.mark_request_as_sent(request_id);
}
}
pub fn invalidate_sessions_new_devices(&self, users: &HashSet<&UserId>) {
for session in self.outbound_group_sessions.iter() {
if users.iter().any(|u| session.contains_recipient(u)) {
info!(
"Invalidating outobund session {} for room {}",
session.session_id(),
session.room_id()
);
session.invalidate_session();
if !session.shared() {
for request_id in session.clear_requests() {
self.outbound_sessions_being_shared.remove(&request_id);
}
}
}
}
}
pub fn get_outbound_group_session(&self, room_id: &RoomId) -> Option<OutboundGroupSession> {
#[allow(clippy::map_clone)]
self.outbound_group_sessions.get(room_id).map(|s| s.clone())
}
pub async fn encrypt(
&self,
room_id: &RoomId,
content: AnyMessageEventContent,
) -> MegolmResult<EncryptedEventContent> {
let session = if let Some(s) = self.get_outbound_group_session(room_id) {
s
} else {
panic!("Session wasn't created nor shared");
};
if session.expired() {
panic!("Session expired");
}
Ok(session.encrypt(content).await)
}
pub fn should_share_group_session(&self, room_id: &RoomId) -> bool {
let session = self.outbound_group_sessions.get(room_id);
match session {
Some(s) => !s.shared() || s.expired() || s.invalidated(),
None => true,
}
}
pub async fn create_outbound_group_session(
&self,
room_id: &RoomId,
settings: EncryptionSettings,
) -> OlmResult<(OutboundGroupSession, InboundGroupSession)> {
let (outbound, inbound) = self
.account
.create_group_session_pair(room_id, settings)
.await
.map_err(|_| EventError::UnsupportedAlgorithm)?;
let _ = self
.outbound_group_sessions
.insert(room_id.to_owned(), outbound.clone());
Ok((outbound, inbound))
}
pub async fn share_group_session(
&self,
room_id: &RoomId,
users: impl Iterator<Item = &UserId>,
encryption_settings: impl Into<EncryptionSettings>,
) -> OlmResult<Vec<Arc<ToDeviceRequest>>> {
let mut changes = Changes::default();
let (session, inbound_session) = self
.create_outbound_group_session(room_id, encryption_settings.into())
.await?;
changes.inbound_group_sessions.push(inbound_session);
let mut devices: Vec<Device> = Vec::new();
for user_id in users {
session.add_recipient(user_id);
let user_devices = self.store.get_user_devices(&user_id).await?;
devices.extend(user_devices.devices().filter(|d| !d.is_blacklisted()));
}
let mut requests = Vec::new();
let key_content = session.as_json().await;
for device_map_chunk in devices.chunks(Self::MAX_TO_DEVICE_MESSAGES) {
let mut messages = BTreeMap::new();
for device in device_map_chunk {
let encrypted = device
.encrypt(EventType::RoomKey, key_content.clone())
.await;
let (used_session, encrypted) = match encrypted {
Ok(c) => c,
Err(OlmError::MissingSession)
| Err(OlmError::EventError(EventError::MissingSenderKey)) => {
continue;
}
Err(e) => return Err(e),
};
changes.sessions.push(used_session);
messages
.entry(device.user_id().clone())
.or_insert_with(BTreeMap::new)
.insert(
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
serde_json::value::to_raw_value(&encrypted)?,
);
}
let id = Uuid::new_v4();
let request = Arc::new(ToDeviceRequest {
event_type: EventType::RoomEncrypted,
txn_id: id,
messages,
});
session.add_request(id, request.clone());
self.outbound_sessions_being_shared
.insert(id, session.clone());
requests.push(request);
}
if requests.is_empty() {
debug!(
"Session {} for room {} doesn't need to be shared with anyone, marking as shared",
session.session_id(),
session.room_id()
);
session.mark_as_shared();
}
self.store.save_changes(changes).await?;
Ok(requests)
}
}