diff --git a/crates/ruma-events/CHANGELOG.md b/crates/ruma-events/CHANGELOG.md index 0a612072..b4c81cd5 100644 --- a/crates/ruma-events/CHANGELOG.md +++ b/crates/ruma-events/CHANGELOG.md @@ -38,6 +38,7 @@ Improvements: * Add unstable blurhash field to member event content struct * Add constructors for the unstable spaces parent and child event content types +* Add unstable support for `m.secret.request` and `m.secret.send` events. Bug fixes: diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs index 367a4af4..ba9ca26f 100644 --- a/crates/ruma-events/src/enums.rs +++ b/crates/ruma-events/src/enums.rs @@ -112,6 +112,12 @@ event_enum! { #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] "m.key.verification.done", "m.room.encrypted", + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + "m.secret.request", + #[cfg(feature = "unstable-pre-spec")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] + "m.secret.send", } } diff --git a/crates/ruma-events/src/lib.rs b/crates/ruma-events/src/lib.rs index a979629a..fdd9c9c2 100644 --- a/crates/ruma-events/src/lib.rs +++ b/crates/ruma-events/src/lib.rs @@ -177,6 +177,9 @@ pub mod room_key; pub mod room_key_request; #[cfg(feature = "unstable-pre-spec")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] +pub mod secret; +#[cfg(feature = "unstable-pre-spec")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))] pub mod space; pub mod sticker; pub mod tag; diff --git a/crates/ruma-events/src/secret.rs b/crates/ruma-events/src/secret.rs new file mode 100644 index 00000000..8d36285d --- /dev/null +++ b/crates/ruma-events/src/secret.rs @@ -0,0 +1,4 @@ +//! Module for events in the *m.secret* namespace. + +pub mod request; +pub mod send; diff --git a/crates/ruma-events/src/secret/request.rs b/crates/ruma-events/src/secret/request.rs new file mode 100644 index 00000000..f7f5ece6 --- /dev/null +++ b/crates/ruma-events/src/secret/request.rs @@ -0,0 +1,300 @@ +//! Types for the *m.secret.request* event. + +use std::convert::TryFrom; + +use ruma_events_macros::EventContent; +use ruma_identifiers::DeviceIdBox; +use ruma_serde::StringEnum; +use serde::{ser::SerializeStruct, Deserialize, Serialize}; + +use crate::ToDeviceEvent; + +/// Event sent by a client to request a secret from another device or to cancel a previous request. +/// +/// It is sent as an unencrypted to-device event. +pub type RequestToDeviceEvent = ToDeviceEvent; + +/// The payload for RequestToDeviceEvent. +#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.secret.request", kind = ToDevice)] +pub struct RequestToDeviceEventContent { + /// The action for the request. + #[serde(flatten)] + pub action: RequestAction, + + /// The ID of the device requesting the event. + pub requesting_device_id: DeviceIdBox, + + /// A random string uniquely identifying (with respect to the requester and the target) the + /// target for a secret. + /// + /// If the secret is requested from multiple devices at the same time, the same ID may be used + /// for every target. The same ID is also used in order to cancel a previous request. + pub request_id: String, +} + +impl RequestToDeviceEventContent { + /// Creates a new `RequestToDeviceEventContent` with the given action, requesting device ID and + /// request ID. + pub fn new( + action: RequestAction, + requesting_device_id: DeviceIdBox, + request_id: String, + ) -> Self { + Self { action, requesting_device_id, request_id } + } +} + +/// Action for an *m.secret.request* event. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(try_from = "RequestActionJsonRepr")] +pub enum RequestAction { + /// Request a secret by its name. + Request(SecretName), + + /// Cancel a request for a secret. + RequestCancellation, + + #[doc(hidden)] + _Custom(String), +} + +impl Serialize for RequestAction { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut st = serializer.serialize_struct("request_action", 2)?; + + match self { + Self::Request(name) => { + st.serialize_field("name", name)?; + st.serialize_field("action", "request")?; + st.end() + } + Self::RequestCancellation => { + st.serialize_field("action", "request_cancellation")?; + st.end() + } + RequestAction::_Custom(custom) => { + st.serialize_field("action", custom)?; + st.end() + } + } + } +} + +#[derive(Deserialize)] +struct RequestActionJsonRepr { + action: String, + name: Option, +} + +impl TryFrom for RequestAction { + type Error = &'static str; + + fn try_from(value: RequestActionJsonRepr) -> Result { + match value.action.as_str() { + "request" => { + if let Some(name) = value.name { + Ok(RequestAction::Request(name)) + } else { + Err("A secret name is required when the action is \"request\".") + } + } + "request_cancellation" => Ok(RequestAction::RequestCancellation), + _ => Ok(RequestAction::_Custom(value.action)), + } + } +} + +/// The name of a secret. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub enum SecretName { + /// Cross-signing master key (m.cross_signing.master). + #[ruma_enum(rename = "m.cross_signing.master")] + CrossSigningMasterKey, + + /// Cross-signing user-signing key (m.cross_signing.user_signing). + #[ruma_enum(rename = "m.cross_signing.user_signing")] + CrossSigningUserSigningKey, + + /// Cross-signing self-signing key (m.cross_signing.self_signing). + #[ruma_enum(rename = "m.cross_signing.self_signing")] + CrossSigningSelfSigningKey, + + /// Recovery key (m.megolm_backup.v1). + #[ruma_enum(rename = "m.megolm_backup.v1")] + RecoveryKey, + + #[doc(hidden)] + _Custom(String), +} + +#[cfg(test)] +mod test { + use super::{RequestAction, RequestToDeviceEventContent, SecretName}; + use matches::assert_matches; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + #[test] + fn secret_request_serialization() { + let content = RequestToDeviceEventContent::new( + RequestAction::Request("org.example.some.secret".into()), + "ABCDEFG".into(), + "randomly_generated_id_9573".into(), + ); + + let json = json!({ + "name": "org.example.some.secret", + "action": "request", + "requesting_device_id": "ABCDEFG", + "request_id": "randomly_generated_id_9573" + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn secret_request_recovery_key_serialization() { + let content = RequestToDeviceEventContent::new( + RequestAction::Request(SecretName::RecoveryKey), + "XYZxyz".into(), + "this_is_a_request_id".into(), + ); + + let json = json!({ + "name": "m.megolm_backup.v1", + "action": "request", + "requesting_device_id": "XYZxyz", + "request_id": "this_is_a_request_id" + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn secret_custom_action_serialization() { + let content = RequestToDeviceEventContent::new( + RequestAction::_Custom("my_custom_action".into()), + "XYZxyz".into(), + "this_is_a_request_id".into(), + ); + + let json = json!({ + "action": "my_custom_action", + "requesting_device_id": "XYZxyz", + "request_id": "this_is_a_request_id" + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn secret_request_cancellation_serialization() { + let content = RequestToDeviceEventContent::new( + RequestAction::RequestCancellation, + "ABCDEFG".into(), + "randomly_generated_id_9573".into(), + ); + + let json = json!({ + "action": "request_cancellation", + "requesting_device_id": "ABCDEFG", + "request_id": "randomly_generated_id_9573" + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn secret_request_deserialization() { + let json = json!({ + "name": "org.example.some.secret", + "action": "request", + "requesting_device_id": "ABCDEFG", + "request_id": "randomly_generated_id_9573" + }); + + assert_matches!( + from_json_value(json).unwrap(), + RequestToDeviceEventContent { + action: RequestAction::Request( + secret + ), + requesting_device_id, + request_id, + } + if secret == "org.example.some.secret".into() + && requesting_device_id == "ABCDEFG" + && request_id == "randomly_generated_id_9573" + ) + } + + #[test] + fn secret_request_cancellation_deserialization() { + let json = json!({ + "action": "request_cancellation", + "requesting_device_id": "ABCDEFG", + "request_id": "randomly_generated_id_9573" + }); + + assert_matches!( + from_json_value(json).unwrap(), + RequestToDeviceEventContent { + action: RequestAction::RequestCancellation, + requesting_device_id, + request_id, + } + if requesting_device_id.as_str() == "ABCDEFG" + && request_id == "randomly_generated_id_9573" + ) + } + + #[test] + fn secret_request_recovery_key_deserialization() { + let json = json!({ + "name": "m.megolm_backup.v1", + "action": "request", + "requesting_device_id": "XYZxyz", + "request_id": "this_is_a_request_id" + }); + + assert_matches!( + from_json_value(json).unwrap(), + RequestToDeviceEventContent { + action: RequestAction::Request( + SecretName::RecoveryKey + ), + requesting_device_id, + request_id, + } + if requesting_device_id == "XYZxyz" + && request_id == "this_is_a_request_id" + ) + } + + #[test] + fn secret_custom_action_deserialization() { + let json = json!({ + "action": "my_custom_action", + "requesting_device_id": "XYZxyz", + "request_id": "this_is_a_request_id" + }); + + assert_matches!( + from_json_value::(json).unwrap(), + RequestToDeviceEventContent { + action, + requesting_device_id, + request_id, + } + if action == RequestAction::_Custom("my_custom_action".into()) + && requesting_device_id == "XYZxyz" + && request_id == "this_is_a_request_id" + ) + } +} diff --git a/crates/ruma-events/src/secret/send.rs b/crates/ruma-events/src/secret/send.rs new file mode 100644 index 00000000..41361425 --- /dev/null +++ b/crates/ruma-events/src/secret/send.rs @@ -0,0 +1,31 @@ +//! Types for the *m.secret.send* event. + +use ruma_events_macros::EventContent; +use serde::{Deserialize, Serialize}; + +use crate::ToDeviceEvent; + +/// An event sent by a client to share a secret with another device, in response to an +/// `m.secret.request` event. +/// +/// It must be encrypted as an `m.room.encrypted` event, then sent as a to-device event. +pub type SendToDeviceEvent = ToDeviceEvent; + +/// The payload for `SendToDeviceEvent`. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.secret.send", kind = ToDevice)] +pub struct SendToDeviceEventContent { + /// The ID of the request that this is a response to. + pub request_id: String, + + /// The contents of the secret. + pub secret: String, +} + +impl SendToDeviceEventContent { + /// Creates a new `SecretSendEventContent` with the given request ID and secret. + pub fn new(request_id: String, secret: String) -> Self { + Self { request_id, secret } + } +}