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.19/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_matches::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    use strass::assert_let;
166
167    use super::{
168        RequestAction, SecretName, SecretRequestAction, ToDeviceSecretRequestEventContent,
169    };
170
171    #[test]
172    fn secret_request_serialization() {
173        let content = ToDeviceSecretRequestEventContent::new(
174            RequestAction::Request(SecretRequestAction::new("org.example.some.secret".into())),
175            "ABCDEFG".into(),
176            "randomly_generated_id_9573".into(),
177        );
178
179        assert_to_canonical_json_eq!(
180            content,
181            json!({
182                "name": "org.example.some.secret",
183                "action": "request",
184                "requesting_device_id": "ABCDEFG",
185                "request_id": "randomly_generated_id_9573",
186            }),
187        );
188    }
189
190    #[test]
191    fn secret_request_recovery_key_serialization() {
192        let content = ToDeviceSecretRequestEventContent::new(
193            RequestAction::Request(SecretRequestAction::new(SecretName::RecoveryKey)),
194            "XYZxyz".into(),
195            "this_is_a_request_id".into(),
196        );
197
198        assert_to_canonical_json_eq!(
199            content,
200            json!({
201                "name": "m.megolm_backup.v1",
202                "action": "request",
203                "requesting_device_id": "XYZxyz",
204                "request_id": "this_is_a_request_id",
205            }),
206        );
207    }
208
209    #[test]
210    fn secret_request_cancellation_serialization() {
211        let content = ToDeviceSecretRequestEventContent::new(
212            RequestAction::RequestCancellation,
213            "ABCDEFG".into(),
214            "randomly_generated_id_9573".into(),
215        );
216
217        assert_to_canonical_json_eq!(
218            content,
219            json!({
220                "action": "request_cancellation",
221                "requesting_device_id": "ABCDEFG",
222                "request_id": "randomly_generated_id_9573",
223            }),
224        );
225    }
226
227    #[test]
228    fn secret_request_deserialization() {
229        let json = json!({
230            "name": "org.example.some.secret",
231            "action": "request",
232            "requesting_device_id": "ABCDEFG",
233            "request_id": "randomly_generated_id_9573"
234        });
235
236        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
237        assert_eq!(content.requesting_device_id, "ABCDEFG");
238        assert_eq!(content.request_id, "randomly_generated_id_9573");
239        assert_let!(RequestAction::Request(secret) = content.action);
240        assert_eq!(secret.name.as_str(), "org.example.some.secret");
241    }
242
243    #[test]
244    fn secret_request_cancellation_deserialization() {
245        let json = json!({
246            "action": "request_cancellation",
247            "requesting_device_id": "ABCDEFG",
248            "request_id": "randomly_generated_id_9573"
249        });
250
251        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
252        assert_eq!(content.requesting_device_id, "ABCDEFG");
253        assert_eq!(content.request_id, "randomly_generated_id_9573");
254        assert_matches!(content.action, RequestAction::RequestCancellation);
255    }
256
257    #[test]
258    fn secret_request_recovery_key_deserialization() {
259        let json = json!({
260            "name": "m.megolm_backup.v1",
261            "action": "request",
262            "requesting_device_id": "XYZxyz",
263            "request_id": "this_is_a_request_id"
264        });
265
266        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
267        assert_eq!(content.requesting_device_id, "XYZxyz");
268        assert_eq!(content.request_id, "this_is_a_request_id");
269        assert_let!(RequestAction::Request(secret) = content.action);
270        assert_eq!(secret.name, SecretName::RecoveryKey);
271    }
272
273    #[test]
274    fn secret_custom_action_serialization_roundtrip() {
275        let json = json!({
276            "action": "my_custom_action",
277            "requesting_device_id": "XYZxyz",
278            "request_id": "this_is_a_request_id"
279        });
280
281        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json.clone()).unwrap();
282        assert_eq!(content.requesting_device_id, "XYZxyz");
283        assert_eq!(content.request_id, "this_is_a_request_id");
284        assert_eq!(content.action.action(), "my_custom_action");
285
286        assert_to_canonical_json_eq!(content, json);
287    }
288}