crypto: Document the Rust side of things

This commit is contained in:
Damir Jelić 2021-04-07 15:04:22 +02:00
parent 336697a38c
commit 533895cb38
4 changed files with 261 additions and 105 deletions

23
rust-sdk/src/device.rs Normal file
View File

@ -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<String, String>,
}
impl From<InnerDevice> 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(),
}
}
}

View File

@ -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 {

View File

@ -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<String, String>,
}
impl From<InnerDevice> 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<Self, MachineCreationError> {
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<Device> {
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<Device> {
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<String, String> {
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<Request> {
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<Sas, CryptoStoreError> {
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<Request> {
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<String>) {
let users: Vec<UserId> = 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<String>) -> Vec<Request> {
let users: Vec<UserId> = 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<String>,
@ -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<String>) -> Vec<Request> {
let users: Vec<UserId> = 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<RawValue> = 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<String, CryptoStoreError> {
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<dyn ProgressListener>,
) -> Result<KeysImportResult, KeyImportError> {
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<String, CryptoStoreError> {
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<dyn ProgressListener>,
) -> Result<KeysImportResult, KeyImportError> {
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<Sas, CryptoStoreError> {
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(),
})
}
}

View File

@ -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<Vec<u8>> {
.expect("Can't create HTTP response")
}
pub enum RequestType {
KeysQuery,
KeysClaim,