ruma_events/key/verification/
start.rs

1//! Types for the [`m.key.verification.start`] event.
2//!
3//! [`m.key.verification.start`]: https://spec.matrix.org/latest/client-server-api/#mkeyverificationstart
4
5use std::{collections::BTreeMap, fmt};
6
7use ruma_common::{serde::Base64, OwnedDeviceId, OwnedTransactionId};
8use ruma_macros::EventContent;
9use serde::{Deserialize, Serialize};
10use serde_json::Value as JsonValue;
11
12use super::{
13    HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString,
14};
15use crate::relation::Reference;
16
17/// The content of a to-device `m.key.verification.start` event.
18///
19/// Begins an SAS key verification process.
20#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22#[ruma_event(type = "m.key.verification.start", kind = ToDevice)]
23pub struct ToDeviceKeyVerificationStartEventContent {
24    /// The device ID which is initiating the process.
25    pub from_device: OwnedDeviceId,
26
27    /// An opaque identifier for the verification process.
28    ///
29    /// Must be unique with respect to the devices involved. Must be the same as the
30    /// `transaction_id` given in the `m.key.verification.request` if this process is originating
31    /// from a request.
32    pub transaction_id: OwnedTransactionId,
33
34    /// Method specific content.
35    #[serde(flatten)]
36    pub method: StartMethod,
37}
38
39impl ToDeviceKeyVerificationStartEventContent {
40    /// Creates a new `ToDeviceKeyVerificationStartEventContent` with the given device ID,
41    /// transaction ID and method specific content.
42    pub fn new(
43        from_device: OwnedDeviceId,
44        transaction_id: OwnedTransactionId,
45        method: StartMethod,
46    ) -> Self {
47        Self { from_device, transaction_id, method }
48    }
49}
50
51/// The content of an in-room `m.key.verification.start` event.
52///
53/// Begins an SAS key verification process.
54#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
55#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
56#[ruma_event(type = "m.key.verification.start", kind = MessageLike)]
57pub struct KeyVerificationStartEventContent {
58    /// The device ID which is initiating the process.
59    pub from_device: OwnedDeviceId,
60
61    /// Method specific content.
62    #[serde(flatten)]
63    pub method: StartMethod,
64
65    /// Information about the related event.
66    #[serde(rename = "m.relates_to")]
67    pub relates_to: Reference,
68}
69
70impl KeyVerificationStartEventContent {
71    /// Creates a new `KeyVerificationStartEventContent` with the given device ID, method and
72    /// reference.
73    pub fn new(from_device: OwnedDeviceId, method: StartMethod, relates_to: Reference) -> Self {
74        Self { from_device, method, relates_to }
75    }
76}
77
78/// An enum representing the different method specific `m.key.verification.start` content.
79#[derive(Clone, Debug, Deserialize, Serialize)]
80#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
81#[serde(untagged)]
82pub enum StartMethod {
83    /// The `m.sas.v1` verification method.
84    SasV1(SasV1Content),
85
86    /// The `m.reciprocate.v1` verification method.
87    ///
88    /// The spec entry for this method can be found [here].
89    ///
90    /// [here]: https://spec.matrix.org/latest/client-server-api/#mkeyverificationstartmreciprocatev1
91    ReciprocateV1(ReciprocateV1Content),
92
93    /// Any unknown start method.
94    #[doc(hidden)]
95    _Custom(_CustomContent),
96}
97
98/// Method specific content of a unknown key verification method.
99#[doc(hidden)]
100#[derive(Clone, Debug, Deserialize, Serialize)]
101#[allow(clippy::exhaustive_structs)]
102pub struct _CustomContent {
103    /// The name of the method.
104    pub method: String,
105
106    /// The additional fields that the method contains.
107    #[serde(flatten)]
108    pub data: BTreeMap<String, JsonValue>,
109}
110
111/// The payload of an `m.key.verification.start` event using the `m.sas.v1` method.
112#[derive(Clone, Deserialize, Serialize)]
113#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
114#[serde(rename = "m.reciprocate.v1", tag = "method")]
115pub struct ReciprocateV1Content {
116    /// The shared secret from the QR code, encoded using unpadded base64.
117    pub secret: Base64,
118}
119
120impl ReciprocateV1Content {
121    /// Create a new `ReciprocateV1Content` with the given shared secret.
122    ///
123    /// The shared secret needs to come from the scanned QR code, encoded using unpadded base64.
124    pub fn new(secret: Base64) -> Self {
125        Self { secret }
126    }
127}
128
129impl fmt::Debug for ReciprocateV1Content {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.debug_struct("ReciprocateV1Content").finish_non_exhaustive()
132    }
133}
134
135/// The payload of an `m.key.verification.start` event using the `m.sas.v1` method.
136///
137/// To create an instance of this type, first create a `SasV1ContentInit` and convert it via
138/// `SasV1Content::from` / `.into()`.
139#[derive(Clone, Debug, Deserialize, Serialize)]
140#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
141#[serde(rename = "m.sas.v1", tag = "method")]
142pub struct SasV1Content {
143    /// The key agreement protocols the sending device understands.
144    ///
145    /// Must include at least `Curve25519` or `Curve25519HkdfSha256`.
146    pub key_agreement_protocols: Vec<KeyAgreementProtocol>,
147
148    /// The hash methods the sending device understands.
149    ///
150    /// Must include at least `sha256`.
151    pub hashes: Vec<HashAlgorithm>,
152
153    /// The message authentication codes that the sending device understands.
154    ///
155    /// Must include at least `hkdf-hmac-sha256.v2`. Should also include `hkdf-hmac-sha256` for
156    /// compatibility with older clients, though this MAC is deprecated and will be removed in a
157    /// future version of the spec.
158    pub message_authentication_codes: Vec<MessageAuthenticationCode>,
159
160    /// The SAS methods the sending device (and the sending device's user) understands.
161    ///
162    /// Must include at least `decimal`. Optionally can include `emoji`.
163    pub short_authentication_string: Vec<ShortAuthenticationString>,
164}
165
166/// Mandatory initial set of fields for creating an `SasV1Content`.
167///
168/// This struct will not be updated even if additional fields are added to `SasV1Content` in a new
169/// (non-breaking) release of the Matrix specification.
170#[derive(Debug)]
171#[allow(clippy::exhaustive_structs)]
172pub struct SasV1ContentInit {
173    /// The key agreement protocols the sending device understands.
174    ///
175    /// Should include at least `curve25519`.
176    pub key_agreement_protocols: Vec<KeyAgreementProtocol>,
177
178    /// The hash methods the sending device understands.
179    ///
180    /// Should include at least `sha256`.
181    pub hashes: Vec<HashAlgorithm>,
182
183    /// The message authentication codes that the sending device understands.
184    ///
185    /// Must include at least `hkdf-hmac-sha256.v2`. Should also include `hkdf-hmac-sha256` for
186    /// compatibility with older clients, though this MAC is deprecated and will be removed in a
187    /// future version of the spec.
188    pub message_authentication_codes: Vec<MessageAuthenticationCode>,
189
190    /// The SAS methods the sending device (and the sending device's user) understands.
191    ///
192    /// Should include at least `decimal`.
193    pub short_authentication_string: Vec<ShortAuthenticationString>,
194}
195
196impl From<SasV1ContentInit> for SasV1Content {
197    /// Creates a new `SasV1Content` from the given init struct.
198    fn from(init: SasV1ContentInit) -> Self {
199        Self {
200            key_agreement_protocols: init.key_agreement_protocols,
201            hashes: init.hashes,
202            message_authentication_codes: init.message_authentication_codes,
203            short_authentication_string: init.short_authentication_string,
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use std::collections::BTreeMap;
211
212    use assert_matches2::assert_matches;
213    use ruma_common::{event_id, serde::Base64};
214    use serde_json::{
215        from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
216    };
217
218    use super::{
219        HashAlgorithm, KeyAgreementProtocol, KeyVerificationStartEventContent,
220        MessageAuthenticationCode, ReciprocateV1Content, SasV1ContentInit,
221        ShortAuthenticationString, StartMethod, ToDeviceKeyVerificationStartEventContent,
222        _CustomContent,
223    };
224    use crate::{relation::Reference, ToDeviceEvent};
225
226    #[test]
227    fn serialization() {
228        let key_verification_start_content = ToDeviceKeyVerificationStartEventContent {
229            from_device: "123".into(),
230            transaction_id: "456".into(),
231            method: StartMethod::SasV1(
232                SasV1ContentInit {
233                    hashes: vec![HashAlgorithm::Sha256],
234                    key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
235                    message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256V2],
236                    short_authentication_string: vec![ShortAuthenticationString::Decimal],
237                }
238                .into(),
239            ),
240        };
241
242        let json_data = json!({
243            "from_device": "123",
244            "transaction_id": "456",
245            "method": "m.sas.v1",
246            "key_agreement_protocols": ["curve25519"],
247            "hashes": ["sha256"],
248            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
249            "short_authentication_string": ["decimal"]
250        });
251
252        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
253
254        let json_data = json!({
255            "from_device": "123",
256            "transaction_id": "456",
257            "method": "m.sas.custom",
258            "test": "field",
259        });
260
261        let key_verification_start_content = ToDeviceKeyVerificationStartEventContent {
262            from_device: "123".into(),
263            transaction_id: "456".into(),
264            method: StartMethod::_Custom(_CustomContent {
265                method: "m.sas.custom".to_owned(),
266                data: vec![("test".to_owned(), JsonValue::from("field"))]
267                    .into_iter()
268                    .collect::<BTreeMap<String, JsonValue>>(),
269            }),
270        };
271
272        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
273
274        {
275            let secret = Base64::new(b"This is a secret to everybody".to_vec());
276
277            let key_verification_start_content = ToDeviceKeyVerificationStartEventContent {
278                from_device: "123".into(),
279                transaction_id: "456".into(),
280                method: StartMethod::ReciprocateV1(ReciprocateV1Content::new(secret.clone())),
281            };
282
283            let json_data = json!({
284                "from_device": "123",
285                "method": "m.reciprocate.v1",
286                "secret": secret,
287                "transaction_id": "456"
288            });
289
290            assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
291        }
292    }
293
294    #[test]
295    fn in_room_serialization() {
296        let event_id = event_id!("$1598361704261elfgc:localhost");
297
298        let key_verification_start_content = KeyVerificationStartEventContent {
299            from_device: "123".into(),
300            relates_to: Reference { event_id: event_id.to_owned() },
301            method: StartMethod::SasV1(
302                SasV1ContentInit {
303                    hashes: vec![HashAlgorithm::Sha256],
304                    key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
305                    message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256V2],
306                    short_authentication_string: vec![ShortAuthenticationString::Decimal],
307                }
308                .into(),
309            ),
310        };
311
312        let json_data = json!({
313            "from_device": "123",
314            "method": "m.sas.v1",
315            "key_agreement_protocols": ["curve25519"],
316            "hashes": ["sha256"],
317            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
318            "short_authentication_string": ["decimal"],
319            "m.relates_to": {
320                "rel_type": "m.reference",
321                "event_id": event_id,
322            }
323        });
324
325        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
326
327        let secret = Base64::new(b"This is a secret to everybody".to_vec());
328
329        let key_verification_start_content = KeyVerificationStartEventContent {
330            from_device: "123".into(),
331            relates_to: Reference { event_id: event_id.to_owned() },
332            method: StartMethod::ReciprocateV1(ReciprocateV1Content::new(secret.clone())),
333        };
334
335        let json_data = json!({
336            "from_device": "123",
337            "method": "m.reciprocate.v1",
338            "secret": secret,
339            "m.relates_to": {
340                "rel_type": "m.reference",
341                "event_id": event_id,
342            }
343        });
344
345        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
346    }
347
348    #[test]
349    fn deserialization() {
350        let json = json!({
351            "from_device": "123",
352            "transaction_id": "456",
353            "method": "m.sas.v1",
354            "hashes": ["sha256"],
355            "key_agreement_protocols": ["curve25519"],
356            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
357            "short_authentication_string": ["decimal"]
358        });
359
360        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
361        let content = from_json_value::<ToDeviceKeyVerificationStartEventContent>(json).unwrap();
362        assert_eq!(content.from_device, "123");
363        assert_eq!(content.transaction_id, "456");
364
365        assert_matches!(content.method, StartMethod::SasV1(sas));
366        assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]);
367        assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]);
368        assert_eq!(
369            sas.message_authentication_codes,
370            vec![MessageAuthenticationCode::HkdfHmacSha256V2]
371        );
372        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
373
374        let json = json!({
375            "content": {
376                "from_device": "123",
377                "transaction_id": "456",
378                "method": "m.sas.v1",
379                "key_agreement_protocols": ["curve25519"],
380                "hashes": ["sha256"],
381                "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
382                "short_authentication_string": ["decimal"]
383            },
384            "type": "m.key.verification.start",
385            "sender": "@example:localhost",
386        });
387
388        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationStartEventContent>>(json)
389            .unwrap();
390        assert_eq!(ev.sender, "@example:localhost");
391        assert_eq!(ev.content.from_device, "123");
392        assert_eq!(ev.content.transaction_id, "456");
393
394        assert_matches!(ev.content.method, StartMethod::SasV1(sas));
395        assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]);
396        assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]);
397        assert_eq!(
398            sas.message_authentication_codes,
399            vec![MessageAuthenticationCode::HkdfHmacSha256V2]
400        );
401        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
402
403        let json = json!({
404            "content": {
405                "from_device": "123",
406                "transaction_id": "456",
407                "method": "m.sas.custom",
408                "test": "field",
409            },
410            "type": "m.key.verification.start",
411            "sender": "@example:localhost",
412        });
413
414        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationStartEventContent>>(json)
415            .unwrap();
416        assert_eq!(ev.sender, "@example:localhost");
417        assert_eq!(ev.content.from_device, "123");
418        assert_eq!(ev.content.transaction_id, "456");
419
420        assert_matches!(ev.content.method, StartMethod::_Custom(custom));
421        assert_eq!(custom.method, "m.sas.custom");
422        assert_eq!(custom.data.get("test"), Some(&JsonValue::from("field")));
423
424        let json = json!({
425            "content": {
426                "from_device": "123",
427                "method": "m.reciprocate.v1",
428                "secret": "c2VjcmV0Cg",
429                "transaction_id": "456",
430            },
431            "type": "m.key.verification.start",
432            "sender": "@example:localhost",
433        });
434
435        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationStartEventContent>>(json)
436            .unwrap();
437        assert_eq!(ev.sender, "@example:localhost");
438        assert_eq!(ev.content.from_device, "123");
439        assert_eq!(ev.content.transaction_id, "456");
440
441        assert_matches!(ev.content.method, StartMethod::ReciprocateV1(reciprocate));
442        assert_eq!(reciprocate.secret.encode(), "c2VjcmV0Cg");
443    }
444
445    #[test]
446    fn in_room_deserialization() {
447        let json = json!({
448            "from_device": "123",
449            "method": "m.sas.v1",
450            "hashes": ["sha256"],
451            "key_agreement_protocols": ["curve25519"],
452            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
453            "short_authentication_string": ["decimal"],
454            "m.relates_to": {
455                "rel_type": "m.reference",
456                "event_id": "$1598361704261elfgc:localhost",
457            }
458        });
459
460        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
461        let content = from_json_value::<KeyVerificationStartEventContent>(json).unwrap();
462        assert_eq!(content.from_device, "123");
463        assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost");
464
465        assert_matches!(content.method, StartMethod::SasV1(sas));
466        assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]);
467        assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]);
468        assert_eq!(
469            sas.message_authentication_codes,
470            vec![MessageAuthenticationCode::HkdfHmacSha256V2]
471        );
472        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
473
474        let json = json!({
475            "from_device": "123",
476            "method": "m.reciprocate.v1",
477            "secret": "c2VjcmV0Cg",
478            "m.relates_to": {
479                "rel_type": "m.reference",
480                "event_id": "$1598361704261elfgc:localhost",
481            }
482        });
483
484        let content = from_json_value::<KeyVerificationStartEventContent>(json).unwrap();
485        assert_eq!(content.from_device, "123");
486        assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost");
487
488        assert_matches!(content.method, StartMethod::ReciprocateV1(reciprocate));
489        assert_eq!(reciprocate.secret.encode(), "c2VjcmV0Cg");
490    }
491}