Skip to content

Commit

Permalink
Add a method to the Device to encrypt an event directly for the device (
Browse files Browse the repository at this point in the history
#3091)

This patch exposes the 1-to-1 encryption method that is usually used to share a room key with a device. Users might want to send encrypted custom to-device events to a device directly, so let's expose this functionality. 

Co-authored-by: Damir Jelić <[email protected]>
  • Loading branch information
BillCarsonFr and poljar authored Feb 8, 2024
1 parent 0c1d90d commit 2e9f362
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 3 deletions.
50 changes: 49 additions & 1 deletion bindings/matrix-sdk-crypto-ffi/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -40,6 +40,7 @@ use ruma::{
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -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<Option<Request>, 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
Expand Down
4 changes: 4 additions & 0 deletions crates/matrix-sdk-crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 42 additions & 1 deletion crates/matrix-sdk-crypto/src/identities/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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<Raw<ToDeviceEncryptedEventContent>> {
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.
Expand Down
97 changes: 96 additions & 1 deletion crates/matrix-sdk-crypto/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
}

0 comments on commit 2e9f362

Please sign in to comment.