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::{OwnedDeviceId, OwnedTransactionId, serde::Base64};
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        Value as JsonValue, from_value as from_json_value, json, to_value as to_json_value,
216    };
217
218    use super::{
219        _CustomContent, HashAlgorithm, KeyAgreementProtocol, KeyVerificationStartEventContent,
220        MessageAuthenticationCode, ReciprocateV1Content, SasV1ContentInit,
221        ShortAuthenticationString, StartMethod, ToDeviceKeyVerificationStartEventContent,
222    };
223    use crate::{ToDeviceEvent, relation::Reference};
224
225    #[test]
226    fn serialization() {
227        let key_verification_start_content = ToDeviceKeyVerificationStartEventContent {
228            from_device: "123".into(),
229            transaction_id: "456".into(),
230            method: StartMethod::SasV1(
231                SasV1ContentInit {
232                    hashes: vec![HashAlgorithm::Sha256],
233                    key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
234                    message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256V2],
235                    short_authentication_string: vec![ShortAuthenticationString::Decimal],
236                }
237                .into(),
238            ),
239        };
240
241        let json_data = json!({
242            "from_device": "123",
243            "transaction_id": "456",
244            "method": "m.sas.v1",
245            "key_agreement_protocols": ["curve25519"],
246            "hashes": ["sha256"],
247            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
248            "short_authentication_string": ["decimal"]
249        });
250
251        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
252
253        let json_data = json!({
254            "from_device": "123",
255            "transaction_id": "456",
256            "method": "m.sas.custom",
257            "test": "field",
258        });
259
260        let key_verification_start_content = ToDeviceKeyVerificationStartEventContent {
261            from_device: "123".into(),
262            transaction_id: "456".into(),
263            method: StartMethod::_Custom(_CustomContent {
264                method: "m.sas.custom".to_owned(),
265                data: vec![("test".to_owned(), JsonValue::from("field"))]
266                    .into_iter()
267                    .collect::<BTreeMap<String, JsonValue>>(),
268            }),
269        };
270
271        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
272
273        {
274            let secret = Base64::new(b"This is a secret to everybody".to_vec());
275
276            let key_verification_start_content = ToDeviceKeyVerificationStartEventContent {
277                from_device: "123".into(),
278                transaction_id: "456".into(),
279                method: StartMethod::ReciprocateV1(ReciprocateV1Content::new(secret.clone())),
280            };
281
282            let json_data = json!({
283                "from_device": "123",
284                "method": "m.reciprocate.v1",
285                "secret": secret,
286                "transaction_id": "456"
287            });
288
289            assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
290        }
291    }
292
293    #[test]
294    fn in_room_serialization() {
295        let event_id = event_id!("$1598361704261elfgc:localhost");
296
297        let key_verification_start_content = KeyVerificationStartEventContent {
298            from_device: "123".into(),
299            relates_to: Reference { event_id: event_id.to_owned() },
300            method: StartMethod::SasV1(
301                SasV1ContentInit {
302                    hashes: vec![HashAlgorithm::Sha256],
303                    key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519],
304                    message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256V2],
305                    short_authentication_string: vec![ShortAuthenticationString::Decimal],
306                }
307                .into(),
308            ),
309        };
310
311        let json_data = json!({
312            "from_device": "123",
313            "method": "m.sas.v1",
314            "key_agreement_protocols": ["curve25519"],
315            "hashes": ["sha256"],
316            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
317            "short_authentication_string": ["decimal"],
318            "m.relates_to": {
319                "rel_type": "m.reference",
320                "event_id": event_id,
321            }
322        });
323
324        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
325
326        let secret = Base64::new(b"This is a secret to everybody".to_vec());
327
328        let key_verification_start_content = KeyVerificationStartEventContent {
329            from_device: "123".into(),
330            relates_to: Reference { event_id: event_id.to_owned() },
331            method: StartMethod::ReciprocateV1(ReciprocateV1Content::new(secret.clone())),
332        };
333
334        let json_data = json!({
335            "from_device": "123",
336            "method": "m.reciprocate.v1",
337            "secret": secret,
338            "m.relates_to": {
339                "rel_type": "m.reference",
340                "event_id": event_id,
341            }
342        });
343
344        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
345    }
346
347    #[test]
348    fn deserialization() {
349        let json = json!({
350            "from_device": "123",
351            "transaction_id": "456",
352            "method": "m.sas.v1",
353            "hashes": ["sha256"],
354            "key_agreement_protocols": ["curve25519"],
355            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
356            "short_authentication_string": ["decimal"]
357        });
358
359        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
360        let content = from_json_value::<ToDeviceKeyVerificationStartEventContent>(json).unwrap();
361        assert_eq!(content.from_device, "123");
362        assert_eq!(content.transaction_id, "456");
363
364        assert_matches!(content.method, StartMethod::SasV1(sas));
365        assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]);
366        assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]);
367        assert_eq!(
368            sas.message_authentication_codes,
369            vec![MessageAuthenticationCode::HkdfHmacSha256V2]
370        );
371        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
372
373        let json = json!({
374            "content": {
375                "from_device": "123",
376                "transaction_id": "456",
377                "method": "m.sas.v1",
378                "key_agreement_protocols": ["curve25519"],
379                "hashes": ["sha256"],
380                "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
381                "short_authentication_string": ["decimal"]
382            },
383            "type": "m.key.verification.start",
384            "sender": "@example:localhost",
385        });
386
387        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationStartEventContent>>(json)
388            .unwrap();
389        assert_eq!(ev.sender, "@example:localhost");
390        assert_eq!(ev.content.from_device, "123");
391        assert_eq!(ev.content.transaction_id, "456");
392
393        assert_matches!(ev.content.method, StartMethod::SasV1(sas));
394        assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]);
395        assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]);
396        assert_eq!(
397            sas.message_authentication_codes,
398            vec![MessageAuthenticationCode::HkdfHmacSha256V2]
399        );
400        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
401
402        let json = json!({
403            "content": {
404                "from_device": "123",
405                "transaction_id": "456",
406                "method": "m.sas.custom",
407                "test": "field",
408            },
409            "type": "m.key.verification.start",
410            "sender": "@example:localhost",
411        });
412
413        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationStartEventContent>>(json)
414            .unwrap();
415        assert_eq!(ev.sender, "@example:localhost");
416        assert_eq!(ev.content.from_device, "123");
417        assert_eq!(ev.content.transaction_id, "456");
418
419        assert_matches!(ev.content.method, StartMethod::_Custom(custom));
420        assert_eq!(custom.method, "m.sas.custom");
421        assert_eq!(custom.data.get("test"), Some(&JsonValue::from("field")));
422
423        let json = json!({
424            "content": {
425                "from_device": "123",
426                "method": "m.reciprocate.v1",
427                "secret": "c2VjcmV0Cg",
428                "transaction_id": "456",
429            },
430            "type": "m.key.verification.start",
431            "sender": "@example:localhost",
432        });
433
434        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationStartEventContent>>(json)
435            .unwrap();
436        assert_eq!(ev.sender, "@example:localhost");
437        assert_eq!(ev.content.from_device, "123");
438        assert_eq!(ev.content.transaction_id, "456");
439
440        assert_matches!(ev.content.method, StartMethod::ReciprocateV1(reciprocate));
441        assert_eq!(reciprocate.secret.encode(), "c2VjcmV0Cg");
442    }
443
444    #[test]
445    fn in_room_deserialization() {
446        let json = json!({
447            "from_device": "123",
448            "method": "m.sas.v1",
449            "hashes": ["sha256"],
450            "key_agreement_protocols": ["curve25519"],
451            "message_authentication_codes": ["hkdf-hmac-sha256.v2"],
452            "short_authentication_string": ["decimal"],
453            "m.relates_to": {
454                "rel_type": "m.reference",
455                "event_id": "$1598361704261elfgc:localhost",
456            }
457        });
458
459        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
460        let content = from_json_value::<KeyVerificationStartEventContent>(json).unwrap();
461        assert_eq!(content.from_device, "123");
462        assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost");
463
464        assert_matches!(content.method, StartMethod::SasV1(sas));
465        assert_eq!(sas.hashes, vec![HashAlgorithm::Sha256]);
466        assert_eq!(sas.key_agreement_protocols, vec![KeyAgreementProtocol::Curve25519]);
467        assert_eq!(
468            sas.message_authentication_codes,
469            vec![MessageAuthenticationCode::HkdfHmacSha256V2]
470        );
471        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
472
473        let json = json!({
474            "from_device": "123",
475            "method": "m.reciprocate.v1",
476            "secret": "c2VjcmV0Cg",
477            "m.relates_to": {
478                "rel_type": "m.reference",
479                "event_id": "$1598361704261elfgc:localhost",
480            }
481        });
482
483        let content = from_json_value::<KeyVerificationStartEventContent>(json).unwrap();
484        assert_eq!(content.from_device, "123");
485        assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost");
486
487        assert_matches!(content.method, StartMethod::ReciprocateV1(reciprocate));
488        assert_eq!(reciprocate.secret.encode(), "c2VjcmV0Cg");
489    }
490}