// Copyright 2020 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::{collections::BTreeMap, sync::Arc, time::Duration}; use dashmap::{DashMap, DashSet}; use matrix_sdk_common::{ api::r0::keys::claim_keys::{Request as KeysClaimRequest, Response as KeysClaimResponse}, assign, identifiers::{DeviceIdBox, DeviceKeyAlgorithm, UserId}, uuid::Uuid, }; use tracing::{error, info, warn}; use crate::{error::OlmResult, key_request::KeyRequestMachine, olm::Account, store::Store}; #[derive(Debug, Clone)] pub(crate) struct SessionManager { account: Account, store: Store, /// A map of user/devices that we need to automatically claim keys for. /// Submodules can insert user/device pairs into this map and the /// user/device paris will be added to the list of users when /// [`get_missing_sessions`](#method.get_missing_sessions) is called. users_for_key_claim: Arc>>, key_request_machine: KeyRequestMachine, } impl SessionManager { const KEY_CLAIM_TIMEOUT: Duration = Duration::from_secs(10); pub fn new( account: Account, users_for_key_claim: Arc>>, key_request_machine: KeyRequestMachine, store: Store, ) -> Self { Self { account, store, key_request_machine, users_for_key_claim, } } /// Get the a key claiming request for the user/device pairs that we are /// missing Olm sessions for. /// /// Returns None if no key claiming request needs to be sent out. /// /// Sessions need to be established between devices so group sessions for a /// room can be shared with them. /// /// This should be called every time a group session needs to be shared as /// well as between sync calls. After a sync some devices may request room /// keys without us having a valid Olm session with them, making it /// impossible to server the room key request, thus it's necessary to check /// for missing sessions between sync as well. /// /// **Note**: Care should be taken that only one such request at a time is /// in flight, e.g. using a lock. /// /// The response of a successful key claiming requests needs to be passed to /// the `OlmMachine` with the [`receive_keys_claim_response`]. /// /// # Arguments /// /// `users` - The list of users that we should check if we lack a session /// with one of their devices. This can be an empty iterator when calling /// this method between sync requests. /// /// [`receive_keys_claim_response`]: #method.receive_keys_claim_response pub async fn get_missing_sessions( &self, users: &mut impl Iterator, ) -> OlmResult> { let mut missing = BTreeMap::new(); // Add the list of devices that the user wishes to establish sessions // right now. for user_id in users { let user_devices = self.store.get_user_devices(user_id).await?; for device in user_devices.devices() { let sender_key = if let Some(k) = device.get_key(DeviceKeyAlgorithm::Curve25519) { k } else { continue; }; let sessions = self.store.get_sessions(sender_key).await?; let is_missing = if let Some(sessions) = sessions { sessions.lock().await.is_empty() } else { true }; if is_missing { missing .entry(user_id.to_owned()) .or_insert_with(BTreeMap::new) .insert( device.device_id().into(), DeviceKeyAlgorithm::SignedCurve25519, ); } } } // Add the list of sessions that for some reason automatically need to // create an Olm session. for item in self.users_for_key_claim.iter() { let user = item.key(); for device_id in item.value().iter() { missing .entry(user.to_owned()) .or_insert_with(BTreeMap::new) .insert(device_id.to_owned(), DeviceKeyAlgorithm::SignedCurve25519); } } if missing.is_empty() { Ok(None) } else { Ok(Some(( Uuid::new_v4(), assign!(KeysClaimRequest::new(missing), { timeout: Some(Self::KEY_CLAIM_TIMEOUT), }), ))) } } /// Receive a successful key claim response and create new Olm sessions with /// the claimed keys. /// /// # Arguments /// /// * `response` - The response containing the claimed one-time keys. pub async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> { // TODO log the failures here for (user_id, user_devices) in &response.one_time_keys { for (device_id, key_map) in user_devices { let device = match self.store.get_readonly_device(&user_id, device_id).await { Ok(Some(d)) => d, Ok(None) => { warn!( "Tried to create an Olm session for {} {}, but the device is unknown", user_id, device_id ); continue; } Err(e) => { warn!( "Tried to create an Olm session for {} {}, but \ can't fetch the device from the store {:?}", user_id, device_id, e ); continue; } }; info!("Creating outbound Session for {} {}", user_id, device_id); let session = match self.account.create_outbound_session(device, &key_map).await { Ok(s) => s, Err(e) => { warn!("{:?}", e); continue; } }; if let Err(e) = self.store.save_sessions(&[session]).await { error!("Failed to store newly created Olm session {}", e); continue; } // TODO if this session was created because a previous one was // wedged queue up a dummy event to be sent out. self.key_request_machine.retry_keyshare(&user_id, device_id); } } Ok(()) } }