ruma_events/key/verification/
accept.rs

1//! Types for the [`m.key.verification.accept`] event.
2//!
3//! [`m.key.verification.accept`]: https://spec.matrix.org/latest/client-server-api/#mkeyverificationaccept
4
5use std::collections::BTreeMap;
6
7use ruma_common::{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.accept` event.
18///
19/// Accepts a previously sent `m.key.verification.start` message.
20#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22#[ruma_event(type = "m.key.verification.accept", kind = ToDevice)]
23pub struct ToDeviceKeyVerificationAcceptEventContent {
24    /// An opaque identifier for the verification process.
25    ///
26    /// Must be the same as the one used for the `m.key.verification.start` message.
27    pub transaction_id: OwnedTransactionId,
28
29    /// The method specific content.
30    #[serde(flatten)]
31    pub method: AcceptMethod,
32}
33
34impl ToDeviceKeyVerificationAcceptEventContent {
35    /// Creates a new `ToDeviceKeyVerificationAcceptEventContent` with the given transaction ID and
36    /// method-specific content.
37    pub fn new(transaction_id: OwnedTransactionId, method: AcceptMethod) -> Self {
38        Self { transaction_id, method }
39    }
40}
41
42/// The content of a in-room `m.key.verification.accept` event.
43///
44/// Accepts a previously sent `m.key.verification.start` message.
45#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
46#[ruma_event(type = "m.key.verification.accept", kind = MessageLike)]
47#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
48pub struct KeyVerificationAcceptEventContent {
49    /// The method specific content.
50    #[serde(flatten)]
51    pub method: AcceptMethod,
52
53    /// Information about the related event.
54    #[serde(rename = "m.relates_to")]
55    pub relates_to: Reference,
56}
57
58impl KeyVerificationAcceptEventContent {
59    /// Creates a new `KeyVerificationAcceptEventContent` with the given method-specific
60    /// content and reference.
61    pub fn new(method: AcceptMethod, relates_to: Reference) -> Self {
62        Self { method, relates_to }
63    }
64}
65
66/// An enum representing the different method specific `m.key.verification.accept` content.
67#[derive(Clone, Debug, Deserialize, Serialize)]
68#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
69#[serde(untagged)]
70pub enum AcceptMethod {
71    /// The `m.sas.v1` verification method.
72    SasV1(SasV1Content),
73
74    /// Any unknown accept method.
75    #[doc(hidden)]
76    _Custom(_CustomContent),
77}
78
79/// Method specific content of a unknown key verification method.
80#[doc(hidden)]
81#[derive(Clone, Debug, Deserialize, Serialize)]
82#[allow(clippy::exhaustive_structs)]
83pub struct _CustomContent {
84    /// The name of the method.
85    pub method: String,
86
87    /// The additional fields that the method contains.
88    #[serde(flatten)]
89    pub data: BTreeMap<String, JsonValue>,
90}
91
92/// The payload of an `m.key.verification.accept` event using the `m.sas.v1` method.
93#[derive(Clone, Debug, Deserialize, Serialize)]
94#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
95#[serde(rename = "m.sas.v1", tag = "method")]
96pub struct SasV1Content {
97    /// The key agreement protocol the device is choosing to use, out of the
98    /// options in the `m.key.verification.start` message.
99    pub key_agreement_protocol: KeyAgreementProtocol,
100
101    /// The hash method the device is choosing to use, out of the options in the
102    /// `m.key.verification.start` message.
103    pub hash: HashAlgorithm,
104
105    /// The message authentication code the device is choosing to use, out of
106    /// the options in the `m.key.verification.start` message.
107    pub message_authentication_code: MessageAuthenticationCode,
108
109    /// The SAS methods both devices involved in the verification process
110    /// understand.
111    ///
112    /// Must be a subset of the options in the `m.key.verification.start`
113    /// message.
114    pub short_authentication_string: Vec<ShortAuthenticationString>,
115
116    /// The hash (encoded as unpadded base64) of the concatenation of the
117    /// device's ephemeral public key (encoded as unpadded base64) and the
118    /// canonical JSON representation of the `m.key.verification.start` message.
119    pub commitment: Base64,
120}
121
122/// Mandatory initial set of fields for creating an accept `SasV1Content`.
123#[derive(Debug)]
124#[allow(clippy::exhaustive_structs)]
125pub struct SasV1ContentInit {
126    /// The key agreement protocol the device is choosing to use, out of the
127    /// options in the `m.key.verification.start` message.
128    pub key_agreement_protocol: KeyAgreementProtocol,
129
130    /// The hash method the device is choosing to use, out of the options in the
131    /// `m.key.verification.start` message.
132    pub hash: HashAlgorithm,
133
134    /// The message authentication codes that the accepting device understands.
135    pub message_authentication_code: MessageAuthenticationCode,
136
137    /// The SAS methods both devices involved in the verification process
138    /// understand.
139    ///
140    /// Must be a subset of the options in the `m.key.verification.start`
141    /// message.
142    pub short_authentication_string: Vec<ShortAuthenticationString>,
143
144    /// The hash (encoded as unpadded base64) of the concatenation of the
145    /// device's ephemeral public key (encoded as unpadded base64) and the
146    /// canonical JSON representation of the `m.key.verification.start` message.
147    pub commitment: Base64,
148}
149
150impl From<SasV1ContentInit> for SasV1Content {
151    /// Creates a new `SasV1Content` from the given init struct.
152    fn from(init: SasV1ContentInit) -> Self {
153        SasV1Content {
154            hash: init.hash,
155            key_agreement_protocol: init.key_agreement_protocol,
156            message_authentication_code: init.message_authentication_code,
157            short_authentication_string: init.short_authentication_string,
158            commitment: init.commitment,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::collections::BTreeMap;
166
167    use assert_matches2::assert_matches;
168    use ruma_common::{
169        canonical_json::assert_to_canonical_json_eq,
170        event_id,
171        serde::{Base64, Raw},
172    };
173    use serde_json::{Value as JsonValue, from_value as from_json_value, json};
174
175    use super::{
176        _CustomContent, AcceptMethod, HashAlgorithm, KeyAgreementProtocol,
177        KeyVerificationAcceptEventContent, MessageAuthenticationCode, SasV1Content,
178        ShortAuthenticationString, ToDeviceKeyVerificationAcceptEventContent,
179    };
180    use crate::{ToDeviceEvent, relation::Reference};
181
182    #[test]
183    fn serialization() {
184        let key_verification_accept_content = ToDeviceKeyVerificationAcceptEventContent {
185            transaction_id: "456".into(),
186            method: AcceptMethod::SasV1(SasV1Content {
187                hash: HashAlgorithm::Sha256,
188                key_agreement_protocol: KeyAgreementProtocol::Curve25519,
189                message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
190                short_authentication_string: vec![ShortAuthenticationString::Decimal],
191                commitment: Base64::new(b"hello".to_vec()),
192            }),
193        };
194
195        assert_to_canonical_json_eq!(
196            key_verification_accept_content,
197            json!({
198                "transaction_id": "456",
199                "method": "m.sas.v1",
200                "commitment": "aGVsbG8",
201                "key_agreement_protocol": "curve25519",
202                "hash": "sha256",
203                "message_authentication_code": "hkdf-hmac-sha256.v2",
204                "short_authentication_string": ["decimal"],
205            }),
206        );
207
208        let key_verification_accept_content = ToDeviceKeyVerificationAcceptEventContent {
209            transaction_id: "456".into(),
210            method: AcceptMethod::_Custom(_CustomContent {
211                method: "m.sas.custom".to_owned(),
212                data: vec![("test".to_owned(), JsonValue::from("field"))]
213                    .into_iter()
214                    .collect::<BTreeMap<String, JsonValue>>(),
215            }),
216        };
217
218        assert_to_canonical_json_eq!(
219            key_verification_accept_content,
220            json!({
221                "transaction_id": "456",
222                "method": "m.sas.custom",
223                "test": "field",
224            }),
225        );
226    }
227
228    #[test]
229    fn in_room_serialization() {
230        let event_id = event_id!("$1598361704261elfgc:localhost");
231
232        let key_verification_accept_content = KeyVerificationAcceptEventContent {
233            relates_to: Reference { event_id: event_id.to_owned() },
234            method: AcceptMethod::SasV1(SasV1Content {
235                hash: HashAlgorithm::Sha256,
236                key_agreement_protocol: KeyAgreementProtocol::Curve25519,
237                message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
238                short_authentication_string: vec![ShortAuthenticationString::Decimal],
239                commitment: Base64::new(b"hello".to_vec()),
240            }),
241        };
242
243        assert_to_canonical_json_eq!(
244            key_verification_accept_content,
245            json!({
246                "method": "m.sas.v1",
247                "commitment": "aGVsbG8",
248                "key_agreement_protocol": "curve25519",
249                "hash": "sha256",
250                "message_authentication_code": "hkdf-hmac-sha256.v2",
251                "short_authentication_string": ["decimal"],
252                "m.relates_to": {
253                    "rel_type": "m.reference",
254                    "event_id": event_id,
255                },
256            }),
257        );
258    }
259
260    #[test]
261    fn deserialization() {
262        let json = json!({
263            "transaction_id": "456",
264            "commitment": "aGVsbG8",
265            "method": "m.sas.v1",
266            "hash": "sha256",
267            "key_agreement_protocol": "curve25519",
268            "message_authentication_code": "hkdf-hmac-sha256.v2",
269            "short_authentication_string": ["decimal"]
270        });
271
272        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
273        let content = from_json_value::<ToDeviceKeyVerificationAcceptEventContent>(json).unwrap();
274        assert_eq!(content.transaction_id, "456");
275
276        assert_matches!(content.method, AcceptMethod::SasV1(sas));
277        assert_eq!(sas.commitment.encode(), "aGVsbG8");
278        assert_eq!(sas.hash, HashAlgorithm::Sha256);
279        assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519);
280        assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
281        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
282
283        let json = json!({
284            "content": {
285                "commitment": "aGVsbG8",
286                "transaction_id": "456",
287                "method": "m.sas.v1",
288                "key_agreement_protocol": "curve25519",
289                "hash": "sha256",
290                "message_authentication_code": "hkdf-hmac-sha256.v2",
291                "short_authentication_string": ["decimal"]
292            },
293            "type": "m.key.verification.accept",
294            "sender": "@example:localhost",
295        });
296
297        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationAcceptEventContent>>(json)
298            .unwrap();
299        assert_eq!(ev.content.transaction_id, "456");
300        assert_eq!(ev.sender, "@example:localhost");
301
302        assert_matches!(ev.content.method, AcceptMethod::SasV1(sas));
303        assert_eq!(sas.commitment.encode(), "aGVsbG8");
304        assert_eq!(sas.hash, HashAlgorithm::Sha256);
305        assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519);
306        assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
307        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
308
309        let json = json!({
310            "content": {
311                "from_device": "123",
312                "transaction_id": "456",
313                "method": "m.sas.custom",
314                "test": "field",
315            },
316            "type": "m.key.verification.accept",
317            "sender": "@example:localhost",
318        });
319
320        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationAcceptEventContent>>(json)
321            .unwrap();
322        assert_eq!(ev.content.transaction_id, "456");
323        assert_eq!(ev.sender, "@example:localhost");
324
325        assert_matches!(ev.content.method, AcceptMethod::_Custom(custom));
326        assert_eq!(custom.method, "m.sas.custom");
327        assert_eq!(custom.data.get("test"), Some(&JsonValue::from("field")));
328    }
329
330    #[test]
331    fn in_room_deserialization() {
332        let json = json!({
333            "commitment": "aGVsbG8",
334            "method": "m.sas.v1",
335            "hash": "sha256",
336            "key_agreement_protocol": "curve25519",
337            "message_authentication_code": "hkdf-hmac-sha256.v2",
338            "short_authentication_string": ["decimal"],
339            "m.relates_to": {
340                "rel_type": "m.reference",
341                "event_id": "$1598361704261elfgc:localhost",
342            }
343        });
344
345        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
346        let content = from_json_value::<KeyVerificationAcceptEventContent>(json).unwrap();
347        assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost");
348
349        assert_matches!(content.method, AcceptMethod::SasV1(sas));
350        assert_eq!(sas.commitment.encode(), "aGVsbG8");
351        assert_eq!(sas.hash, HashAlgorithm::Sha256);
352        assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519);
353        assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
354        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
355    }
356
357    #[test]
358    fn in_room_serialization_roundtrip() {
359        let event_id = event_id!("$1598361704261elfgc:localhost");
360
361        let content = KeyVerificationAcceptEventContent {
362            relates_to: Reference { event_id: event_id.to_owned() },
363            method: AcceptMethod::SasV1(SasV1Content {
364                hash: HashAlgorithm::Sha256,
365                key_agreement_protocol: KeyAgreementProtocol::Curve25519,
366                message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
367                short_authentication_string: vec![ShortAuthenticationString::Decimal],
368                commitment: Base64::new(b"hello".to_vec()),
369            }),
370        };
371
372        let json_content = Raw::new(&content).unwrap();
373        let deser_content = json_content.deserialize().unwrap();
374
375        assert_matches!(deser_content.method, AcceptMethod::SasV1(_));
376        assert_eq!(deser_content.relates_to.event_id, event_id);
377    }
378}