diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 0cbacebbfe9..4e4a772c25e 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -17,7 +17,7 @@ use matrix_sdk_crypto::{ decrypt_room_key_export, encrypt_room_key_export, olm::ExportedRoomKey, store::{BackupDecryptionKey, Changes}, - LocalTrust, OlmMachine as InnerMachine, UserIdentities, + LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities, }; use ruma::{ api::{ @@ -40,6 +40,7 @@ use ruma::{ AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent, }, serde::Raw, + to_device::DeviceIdOrAllDevices, DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId, }; use serde::{Deserialize, Serialize}; @@ -800,6 +801,53 @@ impl OlmMachine { Ok(serde_json::to_string(&encrypted_content)?) } + /// Encrypt the given event with the given type and content for the given + /// device. This method is used to send an event to a specific device. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user who owns the target device. + /// * `device_id` - The ID of the device to which the message will be sent. + /// * `event_type` - The event type. + /// * `content` - The serialized content of the event. + /// + /// # Returns + /// A `Result` containing the request to be sent out if the encryption was + /// successful. If the device is not found, the result will be `Ok(None)`. + /// + /// The caller should ensure that there is an olm session (see + /// `get_missing_sessions`) with the target device before calling this + /// method. + pub fn create_encrypted_to_device_request( + &self, + user_id: String, + device_id: String, + event_type: String, + content: String, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(&user_id)?; + let device_id = device_id.as_str().into(); + let content = serde_json::from_str(&content)?; + + let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?; + + if let Some(device) = device { + let encrypted_content = + self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?; + + let request = ToDeviceRequest::new( + user_id.as_ref(), + DeviceIdOrAllDevices::DeviceId(device_id.to_owned()), + "m.room.encrypted", + encrypted_content.cast(), + ); + + Ok(Some(request.into())) + } else { + Ok(None) + } + } + /// Decrypt the given event that was sent in the given room. /// /// # Arguments diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 0c4bc7327e3..76b64cf274d 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -12,6 +12,10 @@ keys whose metadata lacked check fields. ([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046)) +- Add new API `Device::encrypt_event_raw` that allows + to encrypt an event to a specific device. + ([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091)) + # 0.7.0 - Add method to mark a list of inbound group sessions as backed up: diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index 8f268352922..cdaebf021b5 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -30,12 +30,13 @@ use ruma::{ OwnedDeviceKeyId, UInt, UserId, }; use serde::{Deserialize, Serialize}; +use serde_json::Value; use tokio::sync::Mutex; use tracing::{instrument, trace, warn}; use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey}; use super::{atomic_bool_deserializer, atomic_bool_serializer}; -#[cfg(any(test, feature = "testing"))] +#[cfg(any(test, feature = "testing", doc))] use crate::OlmMachine; use crate::{ error::{EventError, OlmError, OlmResult, SignatureError}, @@ -440,6 +441,46 @@ impl Device { self.encrypt(event_type, content).await } + + /// Encrypt an event for this device. + /// + /// Beware that the 1-to-1 session must be established prior to this + /// call by using the [`OlmMachine::get_missing_sessions`] method. + /// + /// Notable limitation: The caller is responsible for sending the encrypted + /// event to the target device, this encryption method supports out-of-order + /// messages to a certain extent (2000 messages), if multiple messages are + /// encrypted using this method they should be sent in the same order as + /// they are encrypted. + /// + /// *Note*: To instead encrypt an event meant for a room use the + /// [`OlmMachine::encrypt_room_event()`] method instead. + /// + /// # Arguments + /// * `event_type` - The type of the event to be sent. + /// * `content` - The content of the event to be sent. This should be a type + /// that implements the `Serialize` trait. + /// + /// # Returns + /// + /// The encrypted raw content to be shared with your preferred transport + /// layer (usually to-device), [`OlmError::MissingSession`] if there is + /// no established session with the device. + pub async fn encrypt_event_raw( + &self, + event_type: &str, + content: &Value, + ) -> OlmResult> { + let (used_session, raw_encrypted) = self.encrypt(event_type, content).await?; + + // perist the used session + self.verification_machine + .store + .save_changes(Changes { sessions: vec![used_session], ..Default::default() }) + .await?; + + Ok(raw_encrypted) + } } /// A read only view over all devices belonging to a user. diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index fff7865bd48..33584a5c4f3 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -2233,10 +2233,11 @@ pub(crate) mod tests { }, room_id, serde::Raw, + to_device::DeviceIdOrAllDevices, uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceKeyId, SecondsSinceUnixEpoch, TransactionId, UserId, }; - use serde_json::json; + use serde_json::{json, value::to_raw_value}; use vodozemac::{ megolm::{GroupSession, SessionConfig}, Curve25519PublicKey, Ed25519PublicKey, @@ -4242,4 +4243,98 @@ pub(crate) mod tests { .await .unwrap(); } + + #[async_test] + async fn test_send_encrypted_to_device() { + let (alice, bob) = get_machine_pair_with_session(alice_id(), user_id(), false).await; + + let custom_event_type = "m.new_device"; + + let custom_content = json!({ + "device_id": "XYZABCDE", + "rooms": ["!726s6s6q:example.com"] + }); + + let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap(); + let raw_encrypted = device + .encrypt_event_raw(custom_event_type, &custom_content) + .await + .expect("Should have encryted the content"); + + let request = ToDeviceRequest::new( + bob.user_id(), + DeviceIdOrAllDevices::DeviceId(bob_device_id().to_owned()), + "m.room.encrypted", + raw_encrypted.cast(), + ); + + assert_eq!("m.room.encrypted", request.event_type.to_string()); + + let messages = &request.messages; + assert_eq!(1, messages.len()); + assert!(messages.get(bob.user_id()).is_some()); + let target_devices = messages.get(bob.user_id()).unwrap(); + assert_eq!(1, target_devices.len()); + assert!(target_devices + .get(&DeviceIdOrAllDevices::DeviceId(bob_device_id().to_owned())) + .is_some()); + + let event = ToDeviceEvent::new( + alice.user_id().to_owned(), + to_device_requests_to_content(vec![request.clone().into()]), + ); + + let event = json_convert(&event).unwrap(); + + let sync_changes = EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }; + + let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap(); + + assert_eq!(1, decrypted.len()); + + let decrypted_event = decrypted[0].deserialize().unwrap(); + + assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned()); + + let decrypted_value = to_raw_value(&decrypted[0]).unwrap(); + let decrypted_value = serde_json::to_value(decrypted_value).unwrap(); + + assert_eq!( + decrypted_value.get("content").unwrap().get("device_id").unwrap().as_str().unwrap(), + custom_content.get("device_id").unwrap().as_str().unwrap(), + ); + + assert_eq!( + decrypted_value.get("content").unwrap().get("rooms").unwrap().as_array().unwrap(), + custom_content.get("rooms").unwrap().as_array().unwrap(), + ); + } + + #[async_test] + async fn test_send_encrypted_to_device_no_session() { + let (alice, bob, _) = get_machine_pair(alice_id(), user_id(), false).await; + + let custom_event_type = "m.new_device"; + + let custom_content = json!({ + "device_id": "XYZABCDE", + "rooms": ["!726s6s6q:example.com"] + }); + + let encryption_result = alice + .get_device(bob.user_id(), bob_device_id(), None) + .await + .unwrap() + .unwrap() + .encrypt_event_raw(custom_event_type, &custom_content) + .await; + + assert_matches!(encryption_result, Err(OlmError::MissingSession)); + } }