use std::{ collections::{BTreeMap, HashMap}, io::Cursor, mem::ManuallyDrop, ops::Deref, sync::Arc, time::Duration, }; use js_int::UInt; use matrix_sdk_common::deserialized_responses::AlgorithmInfo; use matrix_sdk_crypto::{ backups::{ MegolmV1BackupKey as RustBackupKey, SignatureState, SignatureVerification as RustSignatureCheckResult, }, decrypt_room_key_export, encrypt_room_key_export, olm::ExportedRoomKey, store::{Changes, RecoveryKey}, LocalTrust, OlmMachine as InnerMachine, UserIdentities, }; use ruma::{ api::{ client::{ backup::add_backup_keys::v3::Response as KeysBackupResponse, keys::{ claim_keys::v3::Response as KeysClaimResponse, get_keys::v3::Response as KeysQueryResponse, upload_keys::v3::Response as KeysUploadResponse, upload_signatures::v3::Response as SignatureUploadResponse, }, message::send_message_event::v3::Response as RoomMessageResponse, sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists}, to_device::send_event_to_device::v3::Response as ToDeviceResponse, }, IncomingResponse, }, events::{ key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent, }, serde::Raw, DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId, }; use serde::{Deserialize, Serialize}; use serde_json::{value::RawValue, Value}; use tokio::runtime::Runtime; use zeroize::Zeroize; use crate::{ error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError}, parse_user_id, responses::{response_from_string, OwnedResponse}, BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport, CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings, EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey, ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings, Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest, }; /// A high level state machine that handles E2EE for Matrix. pub struct OlmMachine { pub(crate) inner: ManuallyDrop, pub(crate) runtime: Runtime, } impl Drop for OlmMachine { fn drop(&mut self) { // SAFETY: self.inner is never used again, which is the only requirement // for ManuallyDrop::take to be used safely. let inner = unsafe { ManuallyDrop::take(&mut self.inner) }; let _guard = self.runtime.enter(); // Dropping the inner OlmMachine must happen within a tokio context // because deadpool drops sqlite connections in the DB pool on tokio's // blocking threadpool to avoid blocking async worker threads. drop(inner); } } /// A pair of outgoing room key requests, both of those are sendToDevice /// requests. pub struct KeyRequestPair { /// The optional cancellation, this is None if no previous key request was /// sent out for this key, thus it doesn't need to be cancelled. pub cancellation: Option, /// The actual key request. pub key_request: Request, } /// The result of a signature verification of a signed JSON object. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureVerification { /// The result of the signature verification using the public key of our own /// device. pub device_signature: SignatureState, /// The result of the signature verification using the public key of our own /// user identity. pub user_identity_signature: SignatureState, /// The result of the signature verification using public keys of other /// devices we own. pub other_devices_signatures: HashMap, /// Is the signed JSON object trusted. /// /// This flag tells us if the result has a valid signature from any of the /// following: /// /// * Our own device /// * Our own user identity, provided the identity is trusted as well /// * Any of our own devices, provided the device is trusted as well pub trusted: bool, } impl From for SignatureVerification { fn from(r: RustSignatureCheckResult) -> Self { let trusted = r.trusted(); Self { device_signature: r.device_signature, user_identity_signature: r.user_identity_signature, other_devices_signatures: r .other_signatures .into_iter() .map(|(k, v)| (k.to_string(), v)) .collect(), trusted, } } } #[uniffi::export] 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 our own identity keys. pub fn identity_keys(&self) -> HashMap { let identity_keys = self.inner.identity_keys(); let curve_key = identity_keys.curve25519.to_base64(); let ed25519_key = identity_keys.ed25519.to_base64(); HashMap::from([("ed25519".to_owned(), ed25519_key), ("curve25519".to_owned(), curve_key)]) } /// Get the status of the private cross signing keys. /// /// This can be used to check which private cross signing keys we have /// stored locally. pub fn cross_signing_status(&self) -> CrossSigningStatus { self.runtime.block_on(self.inner.cross_signing_status()).into() } } 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. /// /// * `passphrase` - The passphrase that should be used to encrypt the data /// at rest in the Sled store. **Warning**, if no passphrase is given, the /// store and all its data will remain unencrypted. pub fn new( user_id: &str, device_id: &str, path: &str, mut passphrase: Option, ) -> Result { let user_id = parse_user_id(user_id)?; let device_id = device_id.into(); let runtime = Runtime::new().expect("Couldn't create a tokio runtime"); let store = runtime .block_on(matrix_sdk_sqlite::SqliteCryptoStore::open(path, passphrase.as_deref()))?; passphrase.zeroize(); let inner = runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?; Ok(OlmMachine { inner: ManuallyDrop::new(inner), runtime }) } /// Get the display name of our own device. pub fn display_name(&self) -> Result, CryptoStoreError> { Ok(self.runtime.block_on(self.inner.display_name())?) } /// Get a cross signing user identity for the given user ID. /// /// # Arguments /// /// * `user_id` - The unique id of the user that the identity belongs to /// /// * `timeout` - The time in seconds we should wait before returning if /// the user's device list has been marked as stale. Passing a 0 as the /// timeout means that we won't wait at all. **Note**, this assumes that /// the requests from [`OlmMachine::outgoing_requests`] are being processed /// and sent out. Namely, this waits for a `/keys/query` response to be /// received. pub fn get_identity( &self, user_id: &str, timeout: u32, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(user_id)?; let timeout = if timeout == 0 { None } else { Some(Duration::from_secs(timeout.into())) }; Ok( if let Some(identity) = self.runtime.block_on(self.inner.get_identity(&user_id, timeout))? { Some(self.runtime.block_on(UserIdentity::from_rust(identity))?) } else { None }, ) } /// Check if a user identity is considered to be verified by us. pub fn is_identity_verified(&self, user_id: &str) -> Result { let user_id = parse_user_id(user_id)?; Ok( if let Some(identity) = self.runtime.block_on(self.inner.get_identity(&user_id, None))? { match identity { UserIdentities::Own(i) => i.is_verified(), UserIdentities::Other(i) => i.is_verified(), } } else { false }, ) } /// Manually the user with the given user ID. /// /// This method will attempt to sign the user identity using either our /// private cross signing key, for other user identities, or our device keys /// for our own user identity. /// /// This method can fail if we don't have the private part of our /// user-signing key. /// /// Returns a request that needs to be sent out for the user identity to be /// marked as verified. pub fn verify_identity(&self, user_id: &str) -> Result { let user_id = UserId::parse(user_id)?; let user_identity = self.runtime.block_on(self.inner.get_identity(&user_id, None))?; if let Some(user_identity) = user_identity { Ok(match user_identity { UserIdentities::Own(i) => self.runtime.block_on(i.verify())?, UserIdentities::Other(i) => self.runtime.block_on(i.verify())?, } .into()) } else { Err(SignatureError::UnknownUserIdentity(user_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 itself. /// /// * `timeout` - The time in seconds we should wait before returning if /// the user's device list has been marked as stale. Passing a 0 as the /// timeout means that we won't wait at all. **Note**, this assumes that /// the requests from [`OlmMachine::outgoing_requests`] are being processed /// and sent out. Namely, this waits for a `/keys/query` response to be /// received. pub fn get_device( &self, user_id: &str, device_id: &str, timeout: u32, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(user_id)?; let timeout = if timeout == 0 { None } else { Some(Duration::from_secs(timeout.into())) }; Ok(self .runtime .block_on(self.inner.get_device(&user_id, device_id.into(), timeout))? .map(|d| d.into())) } /// Manually the device of the given user with the given device ID. /// /// This method will attempt to sign the device using our private cross /// signing key. /// /// This method will always fail if the device belongs to someone else, we /// can only sign our own devices. /// /// It can also fail if we don't have the private part of our self-signing /// key. /// /// Returns a request that needs to be sent out for the device to be marked /// as verified. pub fn verify_device( &self, user_id: &str, device_id: &str, ) -> Result { let user_id = UserId::parse(user_id)?; let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?; if let Some(device) = device { Ok(self.runtime.block_on(device.verify())?.into()) } else { Err(SignatureError::UnknownDevice(user_id, device_id.to_owned())) } } /// Set local trust state for the device of the given user without creating /// or uploading any signatures if verified pub fn set_local_trust( &self, user_id: &str, device_id: &str, trust_state: LocalTrust, ) -> Result<(), CryptoStoreError> { let user_id = parse_user_id(user_id)?; let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?; if let Some(device) = device { self.runtime.block_on(device.set_local_trust(trust_state))?; } Ok(()) } /// Get all devices of an user. /// /// # Arguments /// /// * `user_id` - The id of the device owner. /// /// * `timeout` - The time in seconds we should wait before returning if /// the user's device list has been marked as stale. Passing a 0 as the /// timeout means that we won't wait at all. **Note**, this assumes that /// the requests from [`OlmMachine::outgoing_requests`] are being processed /// and sent out. Namely, this waits for a `/keys/query` response to be /// received. pub fn get_user_devices( &self, user_id: &str, timeout: u32, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(user_id)?; let timeout = if timeout == 0 { None } else { Some(Duration::from_secs(timeout.into())) }; Ok(self .runtime .block_on(self.inner.get_user_devices(&user_id, timeout))? .devices() .map(|d| d.into()) .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) -> Result, CryptoStoreError> { Ok(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, request_type: RequestType, response_body: &str, ) -> Result<(), CryptoStoreError> { let id: OwnedTransactionId = request_id.into(); let response = response_from_string(response_body); let response: OwnedResponse = match request_type { RequestType::KeysUpload => { KeysUploadResponse::try_from_http_response(response).map(Into::into) } RequestType::KeysQuery => { KeysQueryResponse::try_from_http_response(response).map(Into::into) } RequestType::ToDevice => { ToDeviceResponse::try_from_http_response(response).map(Into::into) } RequestType::KeysClaim => { KeysClaimResponse::try_from_http_response(response).map(Into::into) } RequestType::SignatureUpload => { SignatureUploadResponse::try_from_http_response(response).map(Into::into) } RequestType::KeysBackup => { KeysBackupResponse::try_from_http_response(response).map(Into::into) } RequestType::RoomMessage => { RoomMessageResponse::try_from_http_response(response).map(Into::into) } } .expect("Can't convert json string to response"); self.runtime.block_on(self.inner.mark_request_as_sent(&id, &response))?; Ok(()) } } #[uniffi::export] impl OlmMachine { /// 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 response. /// /// * `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: String, device_changes: DeviceLists, key_counts: HashMap, unused_fallback_keys: Option>, ) -> Result { let to_device: ToDevice = serde_json::from_str(&events)?; let device_changes: RumaDeviceLists = device_changes.into(); let key_counts: BTreeMap = key_counts .into_iter() .map(|(k, v)| { ( DeviceKeyAlgorithm::from(k), v.clamp(0, i32::MAX) .try_into() .expect("Couldn't convert key counts into an UInt"), ) }) .collect(); let unused_fallback_keys: Option> = unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect()); let events = self.runtime.block_on(self.inner.receive_sync_changes( to_device.events, &device_changes, &key_counts, unused_fallback_keys.as_deref(), ))?; Ok(serde_json::to_string(&events)?) } /// Add the given list of users to be tracked, triggering a key query /// request for them. /// /// The OlmMachine maintains a list of users whose devices we are keeping /// track of: these are known as "tracked users". These must be users /// that we share a room with, so that the server sends us updates for /// their device lists. /// /// *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) -> Result<(), CryptoStoreError> { let users: Vec = users.into_iter().filter_map(|u| UserId::parse(u).ok()).collect(); self.runtime.block_on(self.inner.update_tracked_users(users.iter().map(Deref::deref)))?; Ok(()) } /// Check if the given user is considered to be tracked. /// /// A user can be marked for tracking using the /// [`OlmMachine::update_tracked_users()`] method. pub fn is_user_tracked(&self, user_id: String) -> Result { let user_id = parse_user_id(&user_id)?; Ok(self.runtime.block_on(self.inner.tracked_users())?.contains(&user_id)) } /// 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_room_key()`](#method.share_room_key) 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, ) -> Result, CryptoStoreError> { let users: Vec = users.into_iter().filter_map(|u| UserId::parse(u).ok()).collect(); Ok(self .runtime .block_on(self.inner.get_missing_sessions(users.iter().map(Deref::deref)))? .map(|r| r.into())) } /// Get the stored room settings, such as the encryption algorithm or /// whether to encrypt only for trusted devices. /// /// These settings can be modified via /// [set_room_algorithm()](#method.set_room_algorithm) and /// [set_room_only_allow_trusted_devices()](#method. /// set_room_only_allow_trusted_devices) methods. pub fn get_room_settings( &self, room_id: String, ) -> Result, CryptoStoreError> { let room_id = RoomId::parse(room_id)?; let settings = self .runtime .block_on(self.inner.store().get_room_settings(&room_id))? .map(|v| v.try_into()) .transpose()?; Ok(settings) } /// Set the room algorithm used for encrypting messages to one of the /// available variants pub fn set_room_algorithm( &self, room_id: String, algorithm: EventEncryptionAlgorithm, ) -> Result<(), CryptoStoreError> { let room_id = RoomId::parse(room_id)?; self.runtime.block_on(async move { let mut settings = self.inner.store().get_room_settings(&room_id).await?.unwrap_or_default(); settings.algorithm = algorithm.into(); self.inner .store() .save_changes(Changes { room_settings: HashMap::from([(room_id, settings)]), ..Default::default() }) .await?; Ok(()) }) } /// Set flag whether this room should encrypt messages for untrusted /// devices, or whether they should be excluded from the conversation. /// /// Note that per-room setting may be overridden by a global /// [set_only_allow_trusted_devices()](#method. /// set_only_allow_trusted_devices) method. pub fn set_room_only_allow_trusted_devices( &self, room_id: String, only_allow_trusted_devices: bool, ) -> Result<(), CryptoStoreError> { let room_id = RoomId::parse(room_id)?; self.runtime.block_on(async move { let mut settings = self.inner.store().get_room_settings(&room_id).await?.unwrap_or_default(); settings.only_allow_trusted_devices = only_allow_trusted_devices; self.inner .store() .save_changes(Changes { room_settings: HashMap::from([(room_id, settings)]), ..Default::default() }) .await?; Ok(()) }) } /// Check whether there is a global flag to only encrypt messages for /// trusted devices or for everyone. /// /// Note that if the global flag is false, individual rooms may still be /// encrypting only for trusted devices, depending on the per-room /// `only_allow_trusted_devices` flag. pub fn get_only_allow_trusted_devices(&self) -> Result { let block = self.runtime.block_on(self.inner.store().get_only_allow_trusted_devices())?; Ok(block) } /// Set global flag whether to encrypt messages for untrusted devices, or /// whether they should be excluded from the conversation. /// /// Note that if enabled, it will override any per-room settings. pub fn set_only_allow_trusted_devices( &self, only_allow_trusted_devices: bool, ) -> Result<(), CryptoStoreError> { self.runtime.block_on( self.inner.store().set_only_allow_trusted_devices(only_allow_trusted_devices), )?; Ok(()) } /// Share a room key with the given list of users for the given room. /// /// 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. /// /// * `settings` - The settings that should be used for the room key. pub fn share_room_key( &self, room_id: String, users: Vec, settings: EncryptionSettings, ) -> Result, CryptoStoreError> { let users: Vec = users.into_iter().filter_map(|u| UserId::parse(u).ok()).collect(); let room_id = RoomId::parse(room_id)?; let requests = self.runtime.block_on(self.inner.share_room_key( &room_id, users.iter().map(Deref::deref), settings, ))?; Ok(requests.into_iter().map(|r| r.as_ref().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 event 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_room_key()`](#method.share_room_key). 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: String, event_type: String, content: String, ) -> Result { let room_id = RoomId::parse(room_id)?; let content: Value = serde_json::from_str(&content)?; let encrypted_content = self .runtime .block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type)) .expect("Encrypting an event produced an error"); Ok(serde_json::to_string(&encrypted_content)?) } /// 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. /// /// * `strict_shields` - If `true`, messages will be decorated with strict /// warnings (use `false` to match legacy behaviour where unsafe keys have /// lower severity warnings and unverified identities are not decorated). pub fn decrypt_room_event( &self, event: String, room_id: String, handle_verification_events: bool, strict_shields: bool, ) -> Result { // Element Android wants only the content and the type and will create a // decrypted event with those two itself, this struct makes sure we // throw away all the other fields. #[derive(Deserialize, Serialize)] struct Event<'a> { #[serde(rename = "type")] event_type: String, #[serde(borrow)] content: &'a RawValue, } let event: Raw<_> = serde_json::from_str(&event)?; let room_id = RoomId::parse(room_id)?; let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?; if handle_verification_events { if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() { match &e { AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original( original_event, )) => { if let MessageType::VerificationRequest(_) = &original_event.content.msgtype { self.runtime.block_on(self.inner.receive_verification_event(&e))?; } } _ if e.event_type().to_string().starts_with("m.key.verification") => { self.runtime.block_on(self.inner.receive_verification_event(&e))?; } _ => (), } } } let encryption_info = decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info"); let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?; Ok(match &encryption_info.algorithm_info { AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys } => { DecryptedEvent { clear_event: serde_json::to_string(&event_json)?, sender_curve25519_key: curve25519_key.to_owned(), claimed_ed25519_key: sender_claimed_keys .get(&DeviceKeyAlgorithm::Ed25519) .cloned(), forwarding_curve25519_chain: vec![], shield_state: if strict_shields { encryption_info.verification_state.to_shield_state_strict().into() } else { encryption_info.verification_state.to_shield_state_lax().into() }, } } }) } /// Request or re-request a room key that was used to encrypt the given /// event. /// /// # Arguments /// /// * `event` - The undecryptable event that we would wish to request a room /// key for. /// /// * `room_id` - The id of the room the event was sent to. pub fn request_room_key( &self, event: String, room_id: String, ) -> Result { let event: Raw<_> = serde_json::from_str(&event)?; let room_id = RoomId::parse(room_id)?; let (cancel, request) = self.runtime.block_on(self.inner.request_room_key(&event, &room_id))?; let cancellation = cancel.map(|r| r.into()); let key_request = request.into(); Ok(KeyRequestPair { cancellation, key_request }) } /// 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_room_keys( &self, passphrase: String, rounds: i32, ) -> Result { let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?; let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32) .map_err(CryptoStoreError::Serialization)?; Ok(encrypted) } } impl OlmMachine { fn import_room_keys_helper( &self, keys: Vec, from_backup: bool, progress_listener: Box, ) -> Result { let listener = |progress: usize, total: usize| { progress_listener.on_progress(progress as i32, total as i32) }; let result = self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?; Ok(KeysImportResult { imported: result.imported_count as i64, total: result.total_count as i64, keys: result .keys .into_iter() .map(|(r, m)| { ( r.to_string(), m.into_iter().map(|(s, k)| (s, k.into_iter().collect())).collect(), ) }) .collect(), }) } /// 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_room_keys( &self, keys: &str, passphrase: &str, progress_listener: Box, ) -> Result { let keys = Cursor::new(keys); let keys = decrypt_room_key_export(keys, passphrase)?; self.import_room_keys_helper(keys, false, progress_listener) } /// Import room keys from the given serialized unencrypted key export. /// /// This method is the same as [`OlmMachine::import_room_keys`] but the /// decryption step is skipped and should be performed by the caller. This /// should be used if the room keys are coming from the server-side backup, /// the method will mark all imported room keys as backed up. /// /// # Arguments /// /// * `keys` - The serialized version of the unencrypted key export. /// /// * `progress_listener` - A callback that can be used to introspect the /// progress of the key import. pub fn import_decrypted_room_keys( &self, keys: &str, progress_listener: Box, ) -> Result { let keys: Vec = serde_json::from_str(keys)?; let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect(); self.import_room_keys_helper(keys, true, progress_listener) } } #[uniffi::export] impl OlmMachine { /// Discard the currently active room key for the given room if there is /// one. pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> { let room_id = RoomId::parse(room_id)?; self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?; Ok(()) } /// Receive an unencrypted verification event. /// /// This method can be used to pass verification events that are happening /// in unencrypted rooms to the `OlmMachine`. /// /// **Note**: This has been deprecated. pub fn receive_unencrypted_verification_event( &self, event: String, room_id: String, ) -> Result<(), CryptoStoreError> { self.receive_verification_event(event, room_id) } /// Receive a verification event. /// /// This method can be used to pass verification events that are happening /// in rooms to the `OlmMachine`. The event should be in the decrypted form. pub fn receive_verification_event( &self, event: String, room_id: String, ) -> Result<(), CryptoStoreError> { let room_id = RoomId::parse(room_id)?; let event: AnySyncMessageLikeEvent = serde_json::from_str(&event)?; let event = event.into_full_event(room_id); self.runtime.block_on(self.inner.receive_verification_event(&event))?; Ok(()) } /// Get all the verification requests that we share with the given user. /// /// # Arguments /// /// * `user_id` - The ID of the user for which we would like to fetch the /// verification requests. pub fn get_verification_requests(&self, user_id: String) -> Vec> { let Ok(user_id) = UserId::parse(user_id) else { return vec![]; }; self.inner .get_verification_requests(&user_id) .into_iter() .map(|v| { VerificationRequest { inner: v, runtime: self.runtime.handle().to_owned() }.into() }) .collect() } /// Get a verification requests that we share with the given user with the /// given flow id. /// /// # Arguments /// /// * `user_id` - The ID of the user for which we would like to fetch the /// verification requests. /// /// * `flow_id` - The ID that uniquely identifies the verification flow. pub fn get_verification_request( &self, user_id: String, flow_id: String, ) -> Option> { let user_id = UserId::parse(user_id).ok()?; self.inner.get_verification_request(&user_id, flow_id).map(|v| { VerificationRequest { inner: v, runtime: self.runtime.handle().to_owned() }.into() }) } /// Get an m.key.verification.request content for the given user. /// /// # Arguments /// /// * `user_id` - The ID of the user which we would like to request to /// verify. /// /// * `methods` - The list of verification methods we want to advertise to /// support. pub fn verification_request_content( &self, user_id: String, methods: Vec, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(&user_id)?; let identity = self.runtime.block_on(self.inner.get_identity(&user_id, None))?; let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok(if let Some(identity) = identity.and_then(|i| i.other()) { let content = self.runtime.block_on(identity.verification_request_content(Some(methods))); Some(serde_json::to_string(&content)?) } else { None }) } /// Request a verification flow to begin with the given user in the given /// room. /// /// # Arguments /// /// * `user_id` - The ID of the user which we would like to request to /// verify. /// /// * `room_id` - The ID of the room that represents a DM with the given /// user. /// /// * `event_id` - The event ID of the `m.key.verification.request` event /// that we sent out to request the verification to begin. The content for /// this request can be created using the [verification_request_content()] /// method. /// /// * `methods` - The list of verification methods we advertised as /// supported in the `m.key.verification.request` event. /// /// [verification_request_content()]: #method.verification_request_content pub fn request_verification( &self, user_id: String, room_id: String, event_id: String, methods: Vec, ) -> Result>, CryptoStoreError> { let user_id = parse_user_id(&user_id)?; let event_id = EventId::parse(event_id)?; let room_id = RoomId::parse(room_id)?; let identity = self.runtime.block_on(self.inner.get_identity(&user_id, None))?; let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok(if let Some(identity) = identity.and_then(|i| i.other()) { let request = self.runtime.block_on(identity.request_verification( &room_id, &event_id, Some(methods), )); Some( VerificationRequest { inner: request, runtime: self.runtime.handle().to_owned() } .into(), ) } else { None }) } /// Request a verification flow to begin with the given user's device. /// /// # Arguments /// /// * `user_id` - The ID of the user which we would like to request to /// verify. /// /// * `device_id` - The ID of the device that we wish to verify. /// /// * `methods` - The list of verification methods we advertised as /// supported in the `m.key.verification.request` event. pub fn request_verification_with_device( &self, user_id: String, device_id: String, methods: Vec, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(&user_id)?; let device_id = device_id.as_str().into(); let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok( if let Some(device) = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))? { let (verification, request) = self.runtime.block_on(device.request_verification_with_methods(methods)); Some(RequestVerificationResult { verification: VerificationRequest { inner: verification, runtime: self.runtime.handle().to_owned(), } .into(), request: request.into(), }) } else { None }, ) } /// Request a verification flow to begin with our other devices. /// /// # Arguments /// /// `methods` - The list of verification methods we want to advertise to /// support. pub fn request_self_verification( &self, methods: Vec, ) -> Result, CryptoStoreError> { let identity = self.runtime.block_on(self.inner.get_identity(self.inner.user_id(), None))?; let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok(if let Some(identity) = identity.and_then(|i| i.own()) { let (verification, request) = self.runtime.block_on(identity.request_verification_with_methods(methods))?; Some(RequestVerificationResult { verification: VerificationRequest { inner: verification, runtime: self.runtime.handle().to_owned(), } .into(), request: request.into(), }) } else { None }) } /// Get a verification flow object for the given user with the given flow /// id. /// /// # Arguments /// /// * `user_id` - The ID of the user for which we would like to fetch the /// verification. /// /// * `flow_id` - The ID that uniquely identifies the verification flow. pub fn get_verification(&self, user_id: String, flow_id: String) -> Option> { let user_id = UserId::parse(user_id).ok()?; self.inner .get_verification(&user_id, &flow_id) .map(|v| Verification { inner: v, runtime: self.runtime.handle().to_owned() }.into()) } /// Start short auth string verification with a device without going /// through a verification request first. /// /// **Note**: This has been largely deprecated and the /// [request_verification_with_device()] method should be used instead. /// /// # Arguments /// /// * `user_id` - The ID of the user for which we would like to start the /// SAS verification. /// /// * `device_id` - The ID of device we would like to verify. /// /// [request_verification_with_device()]: #method.request_verification_with_device pub fn start_sas_with_device( &self, user_id: String, device_id: String, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(&user_id)?; let device_id = device_id.as_str().into(); Ok( if let Some(device) = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))? { let (sas, request) = self.runtime.block_on(device.start_verification())?; Some(StartSasResult { sas: Sas { inner: sas, runtime: self.runtime.handle().to_owned() }.into(), request: request.into(), }) } else { None }, ) } /// Create a new private cross signing identity and create a request to /// upload the public part of it to the server. pub fn bootstrap_cross_signing(&self) -> Result { Ok(self.runtime.block_on(self.inner.bootstrap_cross_signing(true))?.into()) } /// Export all our private cross signing keys. /// /// The export will contain the seed for the ed25519 keys as a base64 /// encoded string. /// /// This method returns `None` if we don't have any private cross signing /// keys. pub fn export_cross_signing_keys(&self) -> Option { self.runtime.block_on(self.inner.export_cross_signing_keys()).map(|e| e.into()) } /// Import our private cross signing keys. /// /// The export needs to contain the seed for the ed25519 keys as a base64 /// encoded string. pub fn import_cross_signing_keys( &self, export: CrossSigningKeyExport, ) -> Result<(), SecretImportError> { self.runtime.block_on(self.inner.import_cross_signing_keys(export.into()))?; Ok(()) } /// Activate the given backup key to be used with the given backup version. /// /// **Warning**: The caller needs to make sure that the given `BackupKey` is /// trusted, otherwise we might be encrypting room keys that a malicious /// party could decrypt. /// /// The [`OlmMachine::verify_backup`] method can be used to so. pub fn enable_backup_v1( &self, key: MegolmV1BackupKey, version: String, ) -> Result<(), DecodeError> { let backup_key = RustBackupKey::from_base64(&key.public_key)?; backup_key.set_version(version); self.runtime.block_on(self.inner.backup_machine().enable_backup_v1(backup_key))?; Ok(()) } /// Are we able to encrypt room keys. /// /// This returns true if we have an active `BackupKey` and backup version /// registered with the state machine. pub fn backup_enabled(&self) -> bool { self.runtime.block_on(self.inner.backup_machine().enabled()) } /// Disable and reset our backup state. /// /// This will remove any pending backup request, remove the backup key and /// reset the backup state of each room key we have. pub fn disable_backup(&self) -> Result<(), CryptoStoreError> { Ok(self.runtime.block_on(self.inner.backup_machine().disable_backup())?) } /// Encrypt a batch of room keys and return a request that needs to be sent /// out to backup the room keys. pub fn backup_room_keys(&self) -> Result, CryptoStoreError> { let request = self.runtime.block_on(self.inner.backup_machine().backup())?; let request = request.map(|r| r.into()); Ok(request) } /// Get the number of backed up room keys and the total number of room keys. pub fn room_key_counts(&self) -> Result { Ok(self.runtime.block_on(self.inner.backup_machine().room_key_counts())?.into()) } /// Store the recovery key in the crypto store. /// /// This is useful if the client wants to support gossiping of the backup /// key. pub fn save_recovery_key( &self, key: Option>, version: Option, ) -> Result<(), CryptoStoreError> { let key = key.map(|k| { // We need to clone here due to FFI limitations but RecoveryKey does // not want to expose clone since it's private key material. let mut encoded = k.to_base64(); let key = RecoveryKey::from_base64(&encoded) .expect("Encoding and decoding from base64 should always work"); encoded.zeroize(); key }); Ok(self.runtime.block_on(self.inner.backup_machine().save_recovery_key(key, version))?) } /// Get the backup keys we have saved in our crypto store. pub fn get_backup_keys(&self) -> Result>, CryptoStoreError> { Ok(self .runtime .block_on(self.inner.backup_machine().get_backup_keys())? .try_into() .ok() .map(Arc::new)) } } impl OlmMachine { /// Sign the given message using our device key and if available cross /// signing master key. pub fn sign(&self, message: &str) -> HashMap> { self.runtime .block_on(self.inner.sign(message)) .into_iter() .map(|(k, v)| { ( k.to_string(), v.into_iter() .map(|(k, v)| { ( k.to_string(), match v { Ok(s) => s.to_base64(), Err(i) => i.source, }, ) }) .collect(), ) }) .collect() } /// Check if the given backup has been verified by us or by another of our /// devices that we trust. /// /// The `backup_info` should be a JSON encoded object with the following /// format: /// /// ```json /// { /// "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", /// "auth_data": { /// "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM", /// "signatures": {} /// } /// } /// ``` pub fn verify_backup( &self, backup_info: &str, ) -> Result { let backup_info = serde_json::from_str(backup_info)?; Ok(self .runtime .block_on(self.inner.backup_machine().verify_backup(backup_info, false))? .into()) } }