crypto: Add logic to handle outgoing key requests.

master
Damir Jelić 2020-09-17 16:09:08 +02:00
parent 6b24d91ed9
commit 692f9baa0e
2 changed files with 174 additions and 58 deletions

View File

@ -1,22 +1,28 @@
/// TODO // Copyright 2020 The Matrix.org Foundation C.I.C.
/// //
/// Make a state machine that handles key requests. // Licensed under the Apache License, Version 2.0 (the "License");
/// // you may not use this file except in compliance with the License.
/// Start with the outgoing key requests. We need to queue up a request and // You may obtain a copy of the License at
/// store the outgoing key requests, store if the request was sent out. //
/// Once we receive a key, check if we have an outgoing requests and if so // http://www.apache.org/licenses/LICENSE-2.0
/// accept and store the key if we don't have a better one. Send out a //
/// key request cancelation if we receive one. // Unless required by applicable law or agreed to in writing, software
/// // distributed under the License is distributed on an "AS IS" BASIS,
/// Incoming key requests: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// First handle the easy case, if we trust the device and have a session, queue // See the License for the specific language governing permissions and
/// up a to-device request. // limitations under the License.
///
/// If we don't have a session, queue up a key claim request, once we get a // TODO
/// session send out the key if we trust the device. //
/// // Incoming key requests:
/// If we don't trust the device store an object that remembers the request and // First handle the easy case, if we trust the device and have a session, queue
/// let the users introspect that object. // up a to-device request.
//
// If we don't have a session, queue up a key claim request, once we get a
// session send out the key if we trust the device.
//
// If we don't trust the device store an object that remembers the request and
// let the users introspect that object.
use dashmap::DashMap; use dashmap::DashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::value::to_raw_value; use serde_json::value::to_raw_value;
@ -35,8 +41,9 @@ use matrix_sdk_common::{
}; };
use crate::{ use crate::{
olm::InboundGroupSession,
requests::{OutgoingRequest, ToDeviceRequest}, requests::{OutgoingRequest, ToDeviceRequest},
store::Store, store::{CryptoStoreError, Store},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -77,12 +84,35 @@ impl Encode for ForwardedRoomKeyEventContent {
} }
impl KeyRequestMachine { impl KeyRequestMachine {
pub fn new(user_id: Arc<UserId>, device_id: Arc<DeviceIdBox>, store: Store) -> Self {
Self {
user_id,
device_id,
store,
outgoing_to_device_requests: Arc::new(DashMap::new()),
}
}
/// Create a new outgoing key request for the key with the given session id.
///
/// This will queue up a new to-device request and store the key info so
/// once we receive a forwarded room key we can check that it matches the
/// key we requested.
///
/// This does nothing if a request for this key has already been sent out.
///
/// # Arguments
/// * `room_id` - The id of the room where the key is used in.
///
/// * `sender_key` - The curve25519 key of the sender that owns the key.
///
/// * `session_id` - The id that uniquely identifies the session.
async fn create_outgoing_key_request( async fn create_outgoing_key_request(
&self, &self,
room_id: &RoomId, room_id: &RoomId,
sender_key: &str, sender_key: &str,
session_id: &str, session_id: &str,
) { ) -> Result<(), CryptoStoreError> {
let key_info = RequestedKeyInfo { let key_info = RequestedKeyInfo {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
room_id: room_id.to_owned(), room_id: room_id.to_owned(),
@ -90,11 +120,11 @@ impl KeyRequestMachine {
session_id: session_id.to_owned(), session_id: session_id.to_owned(),
}; };
let id: Option<String> = self.store.get_object(&key_info.encode()).await.unwrap(); let id: Option<String> = self.store.get_object(&key_info.encode()).await?;
if id.is_some() { if id.is_some() {
// We already sent out a request for this key, nothing to do. // We already sent out a request for this key, nothing to do.
return; return Ok(());
} }
info!("Creating new outgoing room key request {:#?}", key_info); info!("Creating new outgoing room key request {:#?}", key_info);
@ -113,10 +143,7 @@ impl KeyRequestMachine {
messages messages
.entry((&*self.user_id).to_owned()) .entry((&*self.user_id).to_owned())
.or_insert_with(BTreeMap::new) .or_insert_with(BTreeMap::new)
.insert( .insert(DeviceIdOrAllDevices::AllDevices, to_raw_value(&content)?);
DeviceIdOrAllDevices::AllDevices,
to_raw_value(&content).unwrap(),
);
let request = OutgoingRequest { let request = OutgoingRequest {
request_id: id, request_id: id,
@ -136,73 +163,125 @@ impl KeyRequestMachine {
sent_out: false, sent_out: false,
}; };
self.save_outgoing_key_info(id, info).await; self.save_outgoing_key_info(id, info).await?;
self.outgoing_to_device_requests.insert(id, request); self.outgoing_to_device_requests.insert(id, request);
Ok(())
} }
async fn save_outgoing_key_info(&self, id: Uuid, info: OugoingKeyInfo) { /// Save an outgoing key info.
// We need to access the key info via the hash of an RequestedKeyInfo async fn save_outgoing_key_info(
// and with the unique request id. &self,
// Once to mark the request as sent. And another time to check if the id: Uuid,
// received forwarded key matches to a request. info: OugoingKeyInfo,
) -> Result<(), CryptoStoreError> {
// TODO we'll want to use a transaction to store those atomically.
// To allow this we'll need to rework our cryptostore trait to return
// a transaction trait and the transaction trait will have the save_X
// methods.
let id_string = id.to_string(); let id_string = id.to_string();
self.store.save_object(&id_string, &info).await.unwrap(); self.store.save_object(&id_string, &info).await?;
self.store self.store.save_object(&info.info.encode(), &id).await?;
.save_object(&info.info.encode(), &id)
.await Ok(())
.unwrap();
} }
async fn get_key_info(&self, content: &ForwardedRoomKeyEventContent) -> Option<OugoingKeyInfo> { /// Get an outgoing key info that matches the forwarded room key content.
let id: Option<Uuid> = self.store.get_object(&content.encode()).await.unwrap(); async fn get_key_info(
&self,
content: &ForwardedRoomKeyEventContent,
) -> Result<Option<OugoingKeyInfo>, CryptoStoreError> {
let id: Option<Uuid> = self.store.get_object(&content.encode()).await?;
if let Some(id) = id { if let Some(id) = id {
self.store.get_object(&id.to_string()).await.unwrap() self.store.get_object(&id.to_string()).await
} else { } else {
None Ok(None)
} }
} }
async fn delete_key_info(&self, info: OugoingKeyInfo) { /// Delete the given outgoing key info.
async fn delete_key_info(&self, info: OugoingKeyInfo) -> Result<(), CryptoStoreError> {
self.store self.store
.delete_object(&info.request_id.to_string()) .delete_object(&info.request_id.to_string())
.await .await?;
.unwrap(); self.store.delete_object(&info.info.encode()).await?;
self.store.delete_object(&info.info.encode()).await.unwrap();
Ok(())
} }
async fn mark_outgoing_request_as_sent(&self, id: Uuid) { /// Mark the outgoing request as sent.
async fn mark_outgoing_request_as_sent(&self, id: Uuid) -> Result<(), CryptoStoreError> {
self.outgoing_to_device_requests.remove(&id); self.outgoing_to_device_requests.remove(&id);
let info: Option<OugoingKeyInfo> = self.store.get_object(&id.to_string()).await.unwrap(); let info: Option<OugoingKeyInfo> = self.store.get_object(&id.to_string()).await?;
if let Some(mut info) = info { if let Some(mut info) = info {
trace!("Marking outgoing key request as sent {:#?}", info); trace!("Marking outgoing key request as sent {:#?}", info);
info.sent_out = true; info.sent_out = true;
self.save_outgoing_key_info(id, info).await; self.save_outgoing_key_info(id, info).await?;
} else { } else {
error!("Trying to mark a room key request with the id {} as sent, but no key info was found", id); error!("Trying to mark a room key request with the id {} as sent, but no key info was found", id);
} }
Ok(())
} }
/// Save an inbound group session we received using a key forward.
///
/// At the same time delete the key info since we received the wanted key.
async fn save_session(
&self,
key_info: OugoingKeyInfo,
session: InboundGroupSession,
) -> Result<(), CryptoStoreError> {
// TODO perhaps only remove the key info if the first known index is 0.
self.store.save_inbound_group_sessions(&[session]).await?;
self.outgoing_to_device_requests
.remove(&key_info.request_id);
self.delete_key_info(key_info).await
}
/// Receive a forwarded room key event.
async fn receive_forwarded_room_key( async fn receive_forwarded_room_key(
&self, &self,
_sender_key: &str, sender_key: &str,
_signing_key: &str,
event: &mut ToDeviceEvent<ForwardedRoomKeyEventContent>, event: &mut ToDeviceEvent<ForwardedRoomKeyEventContent>,
) { ) -> Result<(), CryptoStoreError> {
let key_info = self.get_key_info(&event.content).await; let key_info = self.get_key_info(&event.content).await?;
if let Some(info) = key_info { if let Some(info) = key_info {
// TODO create a new room key and store it if it's a better version let session = InboundGroupSession::from_forwarded_key(sender_key, &event.content)?;
// of the existing key or if we don't have it at all.
self.outgoing_to_device_requests.remove(&info.request_id); let old_session = self
self.delete_key_info(info).await; .store
.get_inbound_group_session(
session.room_id(),
&session.sender_key,
session.session_id(),
)
.await?;
// If we have a previous session, check if we have a better version
// and store the new one if so.
if let Some(old_session) = old_session {
let first_old_index = old_session.first_known_index().await;
let first_index = session.first_known_index().await;
if first_old_index > first_index {
self.save_session(info, session).await?;
}
// If we didn't have a previous session, store it.
} else {
self.save_session(info, session).await?;
}
} else { } else {
info!( info!(
"Received a forwarded room key from {}, but no key info was found.", "Received a forwarded room key from {}, but no key info was found.",
event.sender, event.sender,
); );
} }
Ok(())
} }
fn handle_incoming_key_request(&self, event: &ToDeviceEvent<RoomKeyRequestEventContent>) { fn handle_incoming_key_request(&self, event: &ToDeviceEvent<RoomKeyRequestEventContent>) {

View File

@ -20,7 +20,10 @@ use std::{
}; };
use matrix_sdk_common::{ use matrix_sdk_common::{
events::{room::encrypted::EncryptedEventContent, AnySyncRoomEvent, SyncMessageEvent}, events::{
forwarded_room_key::ForwardedRoomKeyEventContent, room::encrypted::EncryptedEventContent,
AnySyncRoomEvent, SyncMessageEvent,
},
identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId}, identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId},
locks::Mutex, locks::Mutex,
Raw, Raw,
@ -113,6 +116,40 @@ impl InboundGroupSession {
Self::try_from(exported_session.into()) Self::try_from(exported_session.into())
} }
/// Create a new inbound group session from a forwarded room key content.
///
/// # Arguments
///
/// * `sender_key` - The public curve25519 key of the account that
/// sent us the session
///
/// * `content` - A forwarded room key content that contains the session key
/// to create the `InboundGroupSession`.
pub(crate) fn from_forwarded_key(
sender_key: &str,
content: &ForwardedRoomKeyEventContent,
) -> Result<Self, OlmGroupSessionError> {
let session = OlmInboundGroupSession::import(&content.session_key)?;
let mut forwarding_chains = content.forwarding_curve25519_key_chain.clone();
forwarding_chains.push(sender_key.to_owned());
let mut sender_claimed_key = BTreeMap::new();
sender_claimed_key.insert(
DeviceKeyAlgorithm::Ed25519,
content.sender_claimed_ed25519_key.to_owned(),
);
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
session_id: Arc::new(content.session_id.clone()),
sender_key: Arc::new(content.sender_key.clone()),
signing_key: Arc::new(sender_claimed_key),
room_id: Arc::new(content.room_id.clone()),
forwarding_chains: Arc::new(Mutex::new(Some(forwarding_chains))),
imported: Arc::new(true),
})
}
/// Store the group session as a base64 encoded string. /// Store the group session as a base64 encoded string.
/// ///
/// # Arguments /// # Arguments