From 533895cb3867941c11754666953874913be74ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 7 Apr 2021 15:04:22 +0200 Subject: [PATCH] crypto: Document the Rust side of things --- rust-sdk/src/device.rs | 23 +++ rust-sdk/src/lib.rs | 4 +- rust-sdk/src/machine.rs | 329 +++++++++++++++++++++++++++----------- rust-sdk/src/responses.rs | 10 +- 4 files changed, 261 insertions(+), 105 deletions(-) create mode 100644 rust-sdk/src/device.rs diff --git a/rust-sdk/src/device.rs b/rust-sdk/src/device.rs new file mode 100644 index 0000000000..2e3a0a78e1 --- /dev/null +++ b/rust-sdk/src/device.rs @@ -0,0 +1,23 @@ +use std::collections::HashMap; + +use matrix_sdk_crypto::Device as InnerDevice; + +pub struct Device { + pub user_id: String, + pub device_id: String, + pub keys: HashMap, +} + +impl From for Device { + fn from(d: InnerDevice) -> Self { + Device { + user_id: d.user_id().to_string(), + device_id: d.device_id().to_string(), + keys: d + .keys() + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + } + } +} diff --git a/rust-sdk/src/lib.rs b/rust-sdk/src/lib.rs index 44f8daaf64..998182d793 100644 --- a/rust-sdk/src/lib.rs +++ b/rust-sdk/src/lib.rs @@ -1,11 +1,13 @@ +mod device; mod error; mod logger; mod machine; mod responses; +pub use device::Device; pub use error::{CryptoStoreError, DecryptionError, KeyImportError, MachineCreationError}; pub use logger::{set_logger, Logger}; -pub use machine::{Device, OlmMachine, Sas}; +pub use machine::{OlmMachine, Sas}; pub use responses::{DeviceLists, KeysImportResult, Request, RequestType}; pub trait ProgressListener { diff --git a/rust-sdk/src/machine.rs b/rust-sdk/src/machine.rs index 27bc93634b..f6626c2929 100644 --- a/rust-sdk/src/machine.rs +++ b/rust-sdk/src/machine.rs @@ -24,15 +24,14 @@ use matrix_sdk_common::{ }; use matrix_sdk_crypto::{ - decrypt_key_export, encrypt_key_export, Device as InnerDevice, EncryptionSettings, - OlmMachine as InnerMachine, + decrypt_key_export, encrypt_key_export, EncryptionSettings, OlmMachine as InnerMachine, }; use crate::{ error::{CryptoStoreError, DecryptionError, MachineCreationError}, responses::{response_from_string, OwnedResponse}, - DecryptedEvent, DeviceLists, KeyImportError, KeysImportResult, ProgressListener, Request, - RequestType, + DecryptedEvent, Device, DeviceLists, KeyImportError, KeysImportResult, ProgressListener, + Request, RequestType, }; /// A high level state machine that handles E2EE for Matrix. @@ -41,26 +40,6 @@ pub struct OlmMachine { runtime: Runtime, } -pub struct Device { - pub user_id: String, - pub device_id: String, - pub keys: HashMap, -} - -impl From for Device { - fn from(d: InnerDevice) -> Self { - Device { - user_id: d.user_id().to_string(), - device_id: d.device_id().to_string(), - keys: d - .keys() - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - } - } -} - pub struct Sas { pub other_user_id: String, pub other_device_id: String, @@ -69,6 +48,15 @@ pub struct Sas { } impl OlmMachine { + /// Create a new `OlmMachine` + /// + /// # Arguments + /// + /// * `user_id` - The unique ID of the user that owns this machine. + /// + /// * `device_id` - The unique ID of the device that owns this machine. + /// + /// * `path` - The path where the state of the machine should be persisted. pub fn new(user_id: &str, device_id: &str, path: &str) -> Result { let user_id = UserId::try_from(user_id)?; let device_id = device_id.into(); @@ -82,14 +70,23 @@ impl OlmMachine { }) } + /// Get the user ID of the owner of this `OlmMachine`. pub fn user_id(&self) -> String { self.inner.user_id().to_string() } + /// Get the device ID of the device of this `OlmMachine`. pub fn device_id(&self) -> String { self.inner.device_id().to_string() } + /// Get a `Device` from the store. + /// + /// # Arguments + /// + /// * `user_id` - The id of the device owner. + /// + /// * `device_id` - The id of the device itsefl. pub fn get_device(&self, user_id: &str, device_id: &str) -> Option { let user_id = UserId::try_from(user_id).unwrap(); @@ -99,6 +96,11 @@ impl OlmMachine { .map(|d| d.into()) } + /// Get all devices of an user. + /// + /// # Arguments + /// + /// * `user_id` - The id of the device owner. pub fn get_user_devices(&self, user_id: &str) -> Vec { let user_id = UserId::try_from(user_id).unwrap(); self.runtime @@ -109,6 +111,7 @@ impl OlmMachine { .collect() } + /// Get our own identity keys. pub fn identity_keys(&self) -> HashMap { self.inner .identity_keys() @@ -117,6 +120,32 @@ impl OlmMachine { .collect() } + /// Get the list of outgoing requests that need to be sent to the + /// homeserver. + /// + /// After the request was sent out and a successful response was received + /// the response body should be passed back to the state machine using the + /// [mark_request_as_sent()](#method.mark_request_as_sent) method. + /// + /// **Note**: This method call should be locked per call. + pub fn outgoing_requests(&self) -> Vec { + self.runtime + .block_on(self.inner.outgoing_requests()) + .into_iter() + .map(|r| r.into()) + .collect() + } + + /// Mark a request that was sent to the server as sent. + /// + /// # Arguments + /// + /// * `request_id` - The unique ID of the request that was sent out. This + /// needs to be an UUID. + /// + /// * `request_type` - The type of the request that was sent out. + /// + /// * `response_body` - The body of the response that was received. pub fn mark_request_as_sent( &self, request_id: &str, @@ -141,32 +170,21 @@ impl OlmMachine { Ok(()) } - pub fn start_verification(&self, device: &Device) -> Result { - let user_id = UserId::try_from(device.user_id.clone()).unwrap(); - let device_id = device.device_id.as_str().into(); - let device = self - .runtime - .block_on(self.inner.get_device(&user_id, device_id))? - .unwrap(); - - let (sas, request) = self.runtime.block_on(device.start_verification())?; - - Ok(Sas { - other_user_id: sas.other_user_id().to_string(), - other_device_id: sas.other_device_id().to_string(), - flow_id: sas.flow_id().as_str().to_owned(), - request: request.into(), - }) - } - - pub fn outgoing_requests(&self) -> Vec { - self.runtime - .block_on(self.inner.outgoing_requests()) - .into_iter() - .map(|r| r.into()) - .collect() - } - + /// Let the state machine know about E2EE related sync changes that we + /// received from the server. + /// + /// This needs to be called after every sync, ideally before processing + /// any other sync changes. + /// + /// # Arguments + /// + /// * `events` - A serialized array of to-device events we received in the + /// current sync resposne. + /// + /// * `device_changes` - The list of devices that have changed in some way + /// since the previous sync. + /// + /// * `key_counts` - The map of uploaded one-time key types and counts. pub fn receive_sync_changes( &self, events: &str, @@ -195,6 +213,16 @@ impl OlmMachine { .unwrap(); } + /// Add the given list of users to be tracked, triggering a key query request + /// for them. + /// + /// *Note*: Only users that aren't already tracked will be considered for an + /// update. It's safe to call this with already tracked users, it won't + /// result in excessive keys query requests. + /// + /// # Arguments + /// + /// `users` - The users that should be queued up for a key query. pub fn update_tracked_users(&self, users: Vec) { let users: Vec = users .into_iter() @@ -205,25 +233,20 @@ impl OlmMachine { .block_on(self.inner.update_tracked_users(users.iter())); } - pub fn share_group_session(&self, room_id: &str, users: Vec) -> Vec { - let users: Vec = users - .into_iter() - .filter_map(|u| UserId::try_from(u).ok()) - .collect(); - - let room_id = RoomId::try_from(room_id).unwrap(); - let requests = self - .runtime - .block_on(self.inner.share_group_session( - &room_id, - users.iter(), - EncryptionSettings::default(), - )) - .unwrap(); - - requests.into_iter().map(|r| (&*r).into()).collect() - } - + /// Generate one-time key claiming requests for all the users we are missing + /// sessions for. + /// + /// After the request was sent out and a successful response was received + /// the response body should be passed back to the state machine using the + /// [mark_request_as_sent()](#method.mark_request_as_sent) method. + /// + /// This method should be called every time before a call to + /// [`share_group_session()`](#method.share_group_session) is made. + /// + /// # Arguments + /// + /// * `users` - The list of users for which we would like to establish 1:1 + /// Olm sessions for. pub fn get_missing_sessions( &self, users: Vec, @@ -253,6 +276,76 @@ impl OlmMachine { })) } + /// Generate one-time key claiming requests for all the users we are missing + /// sessions for. + /// + /// After the request was sent out and a successful response was received + /// the response body should be passed back to the state machine using the + /// [mark_request_as_sent()](#method.mark_request_as_sent) method. + /// + /// This method should be called every time before a call to + /// [`encrypt()`](#method.encrypt) with the given `room_id` is made. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room, note that this doesn't strictly + /// need to be a Matrix room, it just needs to be an unique identifier for + /// the group that will participate in the conversation. + /// + /// * `users` - The list of users which are considered to be members of the + /// room and should receive the room key. + pub fn share_group_session(&self, room_id: &str, users: Vec) -> Vec { + let users: Vec = users + .into_iter() + .filter_map(|u| UserId::try_from(u).ok()) + .collect(); + + let room_id = RoomId::try_from(room_id).unwrap(); + let requests = self + .runtime + .block_on(self.inner.share_group_session( + &room_id, + users.iter(), + EncryptionSettings::default(), + )) + .unwrap(); + + requests.into_iter().map(|r| (&*r).into()).collect() + } + + /// Encrypt the given event with the given type and content for the given + /// room. + /// + /// **Note**: A room key needs to be shared with the group of users that are + /// members in the given room. If this is not done this method will panic. + /// + /// The usual flow to encrypt an evnet using this state machine is as + /// follows: + /// + /// 1. Get the one-time key claim request to establish 1:1 Olm sessions for + /// the room members of the room we wish to participate in. This is done + /// using the [`get_missing_sessions()`](#method.get_missing_sessions) + /// method. This method call should be locked per call. + /// + /// 2. Share a room key with all the room members using the + /// [`share_group_session()`](#method.share_group_session). This method + /// call should be locked per room. + /// + /// 3. Encrypt the event using this method. + /// + /// 4. Send the encrypted event to the server. + /// + /// After the room key is shared steps 1 and 2 will become noops, unless + /// there's some changes in the room membership or in the list of devices a + /// member has. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room where the event will be sent to. + /// + /// * `even_type` - The type of the event. + /// + /// * `content` - The serialized content of the event. pub fn encrypt(&self, room_id: &str, event_type: &str, content: &str) -> String { let room_id = RoomId::try_from(room_id).unwrap(); let content: Box = serde_json::from_str(content).unwrap(); @@ -266,33 +359,13 @@ impl OlmMachine { serde_json::to_string(&encrypted_content).unwrap() } - pub fn export_keys(&self, passphrase: &str, rounds: i32) -> Result { - let keys = self.runtime.block_on(self.inner.export_keys(|_| true))?; - - let encrypted = encrypt_key_export(&keys, passphrase, rounds as u32) - .map_err(CryptoStoreError::Serialization)?; - - Ok(encrypted) - } - - pub fn import_keys( - &self, - keys: &str, - passphrase: &str, - _: Box, - ) -> Result { - let keys = Cursor::new(keys); - let keys = decrypt_key_export(keys, passphrase)?; - - // TODO use the progress listener - let result = self.runtime.block_on(self.inner.import_keys(keys))?; - - Ok(KeysImportResult { - total: result.1 as i32, - imported: result.0 as i32, - }) - } - + /// Decrypt the given event that was sent in the given room. + /// + /// # Arguments + /// + /// * `event` - The serialized encrypted version of the event. + /// + /// * `room_id` - The unique id of the room where the event was sent to. pub fn decrypt_room_event( &self, event: &str, @@ -331,4 +404,68 @@ impl OlmMachine { }, }) } + + /// Export all of our room keys. + /// + /// # Arguments + /// + /// * `passphrase` - The passphrase that should be used to encrypt the key + /// export. + /// + /// * `rounds` - The number of rounds that should be used when expanding the + /// passphrase into an key. + pub fn export_keys(&self, passphrase: &str, rounds: i32) -> Result { + let keys = self.runtime.block_on(self.inner.export_keys(|_| true))?; + + let encrypted = encrypt_key_export(&keys, passphrase, rounds as u32) + .map_err(CryptoStoreError::Serialization)?; + + Ok(encrypted) + } + + /// Import room keys from the given serialized key export. + /// + /// # Arguments + /// + /// * `keys` - The serialized version of the key export. + /// + /// * `passphrase` - The passphrase that was used to encrypt the key export. + /// + /// * `progress_listener` - A callback that can be used to introspect the + /// progress of the key import. + pub fn import_keys( + &self, + keys: &str, + passphrase: &str, + _progress_listener: Box, + ) -> Result { + let keys = Cursor::new(keys); + let keys = decrypt_key_export(keys, passphrase)?; + + // TODO use the progress listener + let result = self.runtime.block_on(self.inner.import_keys(keys))?; + + Ok(KeysImportResult { + total: result.1 as i32, + imported: result.0 as i32, + }) + } + + pub fn start_verification(&self, device: &Device) -> Result { + let user_id = UserId::try_from(device.user_id.clone()).unwrap(); + let device_id = device.device_id.as_str().into(); + let device = self + .runtime + .block_on(self.inner.get_device(&user_id, device_id))? + .unwrap(); + + let (sas, request) = self.runtime.block_on(device.start_verification())?; + + Ok(Sas { + other_user_id: sas.other_user_id().to_string(), + other_device_id: sas.other_device_id().to_string(), + flow_id: sas.flow_id().as_str().to_owned(), + request: request.into(), + }) + } } diff --git a/rust-sdk/src/responses.rs b/rust-sdk/src/responses.rs index 4861920598..d7d52fef98 100644 --- a/rust-sdk/src/responses.rs +++ b/rust-sdk/src/responses.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashMap, - convert::TryFrom, -}; +use std::{collections::HashMap, convert::TryFrom}; use http::Response; use serde_json::json; @@ -19,9 +16,7 @@ use matrix_sdk_common::{ identifiers::UserId, }; -use matrix_sdk_crypto::{ - IncomingResponse, OutgoingRequest, ToDeviceRequest, -}; +use matrix_sdk_crypto::{IncomingResponse, OutgoingRequest, ToDeviceRequest}; pub enum Request { ToDevice { @@ -101,7 +96,6 @@ pub(crate) fn response_from_string(body: &str) -> Response> { .expect("Can't create HTTP response") } - pub enum RequestType { KeysQuery, KeysClaim,