Skip to main content

ruma_events/secret/
request.rs

1//! Types for the [`m.secret.request`] event.
2//!
3//! [`m.secret.request`]: https://spec.matrix.org/v1.18/client-server-api/#msecretrequest
4
5use as_variant::as_variant;
6use ruma_common::{
7    OwnedDeviceId, OwnedTransactionId,
8    serde::{JsonObject, StringEnum},
9};
10use ruma_macros::EventContent;
11use serde::{Deserialize, Serialize, de};
12use serde_json::{Value as JsonValue, from_value as from_json_value};
13
14use crate::{GlobalAccountDataEventType, PrivOwnedStr};
15
16/// The content of an `m.secret.request` event.
17///
18/// Event sent by a client to request a secret from another device or to cancel a previous request.
19///
20/// It is sent as an unencrypted to-device event.
21#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
22#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
23#[ruma_event(type = "m.secret.request", kind = ToDevice)]
24pub struct ToDeviceSecretRequestEventContent {
25    /// The action for the request.
26    #[serde(flatten)]
27    pub action: RequestAction,
28
29    /// The ID of the device requesting the event.
30    pub requesting_device_id: OwnedDeviceId,
31
32    /// A random string uniquely identifying (with respect to the requester and the target) the
33    /// target for a secret.
34    ///
35    /// If the secret is requested from multiple devices at the same time, the same ID may be used
36    /// for every target. The same ID is also used in order to cancel a previous request.
37    pub request_id: OwnedTransactionId,
38}
39
40impl ToDeviceSecretRequestEventContent {
41    /// Creates a new `ToDeviceRequestEventContent` with the given action, requesting device ID and
42    /// request ID.
43    pub fn new(
44        action: RequestAction,
45        requesting_device_id: OwnedDeviceId,
46        request_id: OwnedTransactionId,
47    ) -> Self {
48        Self { action, requesting_device_id, request_id }
49    }
50}
51
52/// Action for an `m.secret.request` event.
53#[derive(Clone, Debug, Serialize)]
54#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
55#[serde(tag = "action", rename_all = "snake_case")]
56pub enum RequestAction {
57    /// Request a secret by its name.
58    Request(SecretRequestAction),
59
60    /// Cancel a request for a secret.
61    RequestCancellation,
62
63    #[doc(hidden)]
64    #[serde(untagged)]
65    _Custom(CustomRequestAction),
66}
67
68impl RequestAction {
69    /// Access the `action` field of this action.
70    pub fn action(&self) -> &str {
71        match self {
72            Self::Request(_) => "request",
73            Self::RequestCancellation => "request_cancellation",
74            Self::_Custom(custom) => &custom.action,
75        }
76    }
77}
78
79impl<'de> Deserialize<'de> for RequestAction {
80    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81    where
82        D: serde::Deserializer<'de>,
83    {
84        let mut json = JsonObject::deserialize(deserializer)?;
85
86        let action = json
87            .remove("action")
88            .and_then(|value| as_variant!(value, JsonValue::String))
89            .ok_or_else(|| de::Error::missing_field("action"))?;
90
91        match action.as_ref() {
92            "request" => from_json_value(json.into()).map(Self::Request),
93            "request_cancellation" => Ok(Self::RequestCancellation),
94            _ => Ok(Self::_Custom(CustomRequestAction { action })),
95        }
96        .map_err(de::Error::custom)
97    }
98}
99
100impl From<SecretRequestAction> for RequestAction {
101    fn from(value: SecretRequestAction) -> Self {
102        Self::Request(value)
103    }
104}
105
106/// Details about a secret to request in a [`RequestAction`].
107#[derive(Clone, Debug, Serialize, Deserialize)]
108#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
109pub struct SecretRequestAction {
110    /// The name of the requested secret.
111    pub name: SecretName,
112}
113
114impl SecretRequestAction {
115    /// Construct a new `SecretRequestAction` for the given secret name.
116    pub fn new(name: SecretName) -> Self {
117        Self { name }
118    }
119}
120
121/// The name of a secret.
122#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
123#[derive(Clone, StringEnum)]
124#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
125pub enum SecretName {
126    /// Cross-signing master key (m.cross_signing.master).
127    #[ruma_enum(rename = "m.cross_signing.master")]
128    CrossSigningMasterKey,
129
130    /// Cross-signing user-signing key (m.cross_signing.user_signing).
131    #[ruma_enum(rename = "m.cross_signing.user_signing")]
132    CrossSigningUserSigningKey,
133
134    /// Cross-signing self-signing key (m.cross_signing.self_signing).
135    #[ruma_enum(rename = "m.cross_signing.self_signing")]
136    CrossSigningSelfSigningKey,
137
138    /// Recovery key (m.megolm_backup.v1).
139    #[ruma_enum(rename = "m.megolm_backup.v1")]
140    RecoveryKey,
141
142    #[doc(hidden)]
143    _Custom(PrivOwnedStr),
144}
145
146impl From<SecretName> for GlobalAccountDataEventType {
147    fn from(value: SecretName) -> Self {
148        GlobalAccountDataEventType::from(value.as_str())
149    }
150}
151
152/// A custom [`RequestAction`].
153#[doc(hidden)]
154#[derive(Clone, Debug, Serialize)]
155pub struct CustomRequestAction {
156    /// The action of the request.
157    action: String,
158}
159
160#[cfg(test)]
161mod tests {
162    use assert_matches2::assert_matches;
163    use ruma_common::canonical_json::assert_to_canonical_json_eq;
164    use serde_json::{from_value as from_json_value, json};
165
166    use super::{
167        RequestAction, SecretName, SecretRequestAction, ToDeviceSecretRequestEventContent,
168    };
169
170    #[test]
171    fn secret_request_serialization() {
172        let content = ToDeviceSecretRequestEventContent::new(
173            RequestAction::Request(SecretRequestAction::new("org.example.some.secret".into())),
174            "ABCDEFG".into(),
175            "randomly_generated_id_9573".into(),
176        );
177
178        assert_to_canonical_json_eq!(
179            content,
180            json!({
181                "name": "org.example.some.secret",
182                "action": "request",
183                "requesting_device_id": "ABCDEFG",
184                "request_id": "randomly_generated_id_9573",
185            }),
186        );
187    }
188
189    #[test]
190    fn secret_request_recovery_key_serialization() {
191        let content = ToDeviceSecretRequestEventContent::new(
192            RequestAction::Request(SecretRequestAction::new(SecretName::RecoveryKey)),
193            "XYZxyz".into(),
194            "this_is_a_request_id".into(),
195        );
196
197        assert_to_canonical_json_eq!(
198            content,
199            json!({
200                "name": "m.megolm_backup.v1",
201                "action": "request",
202                "requesting_device_id": "XYZxyz",
203                "request_id": "this_is_a_request_id",
204            }),
205        );
206    }
207
208    #[test]
209    fn secret_request_cancellation_serialization() {
210        let content = ToDeviceSecretRequestEventContent::new(
211            RequestAction::RequestCancellation,
212            "ABCDEFG".into(),
213            "randomly_generated_id_9573".into(),
214        );
215
216        assert_to_canonical_json_eq!(
217            content,
218            json!({
219                "action": "request_cancellation",
220                "requesting_device_id": "ABCDEFG",
221                "request_id": "randomly_generated_id_9573",
222            }),
223        );
224    }
225
226    #[test]
227    fn secret_request_deserialization() {
228        let json = json!({
229            "name": "org.example.some.secret",
230            "action": "request",
231            "requesting_device_id": "ABCDEFG",
232            "request_id": "randomly_generated_id_9573"
233        });
234
235        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
236        assert_eq!(content.requesting_device_id, "ABCDEFG");
237        assert_eq!(content.request_id, "randomly_generated_id_9573");
238        assert_matches!(content.action, RequestAction::Request(secret));
239        assert_eq!(secret.name.as_str(), "org.example.some.secret");
240    }
241
242    #[test]
243    fn secret_request_cancellation_deserialization() {
244        let json = json!({
245            "action": "request_cancellation",
246            "requesting_device_id": "ABCDEFG",
247            "request_id": "randomly_generated_id_9573"
248        });
249
250        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
251        assert_eq!(content.requesting_device_id, "ABCDEFG");
252        assert_eq!(content.request_id, "randomly_generated_id_9573");
253        assert_matches!(content.action, RequestAction::RequestCancellation);
254    }
255
256    #[test]
257    fn secret_request_recovery_key_deserialization() {
258        let json = json!({
259            "name": "m.megolm_backup.v1",
260            "action": "request",
261            "requesting_device_id": "XYZxyz",
262            "request_id": "this_is_a_request_id"
263        });
264
265        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
266        assert_eq!(content.requesting_device_id, "XYZxyz");
267        assert_eq!(content.request_id, "this_is_a_request_id");
268        assert_matches!(content.action, RequestAction::Request(secret));
269        assert_eq!(secret.name, SecretName::RecoveryKey);
270    }
271
272    #[test]
273    fn secret_custom_action_serialization_roundtrip() {
274        let json = json!({
275            "action": "my_custom_action",
276            "requesting_device_id": "XYZxyz",
277            "request_id": "this_is_a_request_id"
278        });
279
280        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json.clone()).unwrap();
281        assert_eq!(content.requesting_device_id, "XYZxyz");
282        assert_eq!(content.request_id, "this_is_a_request_id");
283        assert_eq!(content.action.action(), "my_custom_action");
284
285        assert_to_canonical_json_eq!(content, json);
286    }
287}