ruma_events/secret/
request.rs

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