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::{serde::Base64, 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.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 `ToDeviceKeyVerificationAcceptEventContent` 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        event_id,
170        serde::{Base64, Raw},
171    };
172    use serde_json::{
173        from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
174    };
175
176    use super::{
177        AcceptMethod, HashAlgorithm, KeyAgreementProtocol, KeyVerificationAcceptEventContent,
178        MessageAuthenticationCode, SasV1Content, ShortAuthenticationString,
179        ToDeviceKeyVerificationAcceptEventContent, _CustomContent,
180    };
181    use crate::{relation::Reference, ToDeviceEvent};
182
183    #[test]
184    fn serialization() {
185        let key_verification_accept_content = ToDeviceKeyVerificationAcceptEventContent {
186            transaction_id: "456".into(),
187            method: AcceptMethod::SasV1(SasV1Content {
188                hash: HashAlgorithm::Sha256,
189                key_agreement_protocol: KeyAgreementProtocol::Curve25519,
190                message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
191                short_authentication_string: vec![ShortAuthenticationString::Decimal],
192                commitment: Base64::new(b"hello".to_vec()),
193            }),
194        };
195
196        let json_data = json!({
197            "transaction_id": "456",
198            "method": "m.sas.v1",
199            "commitment": "aGVsbG8",
200            "key_agreement_protocol": "curve25519",
201            "hash": "sha256",
202            "message_authentication_code": "hkdf-hmac-sha256.v2",
203            "short_authentication_string": ["decimal"]
204        });
205
206        assert_eq!(to_json_value(&key_verification_accept_content).unwrap(), json_data);
207
208        let json_data = json!({
209            "transaction_id": "456",
210            "method": "m.sas.custom",
211            "test": "field",
212        });
213
214        let key_verification_accept_content = ToDeviceKeyVerificationAcceptEventContent {
215            transaction_id: "456".into(),
216            method: AcceptMethod::_Custom(_CustomContent {
217                method: "m.sas.custom".to_owned(),
218                data: vec![("test".to_owned(), JsonValue::from("field"))]
219                    .into_iter()
220                    .collect::<BTreeMap<String, JsonValue>>(),
221            }),
222        };
223
224        assert_eq!(to_json_value(&key_verification_accept_content).unwrap(), json_data);
225    }
226
227    #[test]
228    fn in_room_serialization() {
229        let event_id = event_id!("$1598361704261elfgc:localhost");
230
231        let key_verification_accept_content = KeyVerificationAcceptEventContent {
232            relates_to: Reference { event_id: event_id.to_owned() },
233            method: AcceptMethod::SasV1(SasV1Content {
234                hash: HashAlgorithm::Sha256,
235                key_agreement_protocol: KeyAgreementProtocol::Curve25519,
236                message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
237                short_authentication_string: vec![ShortAuthenticationString::Decimal],
238                commitment: Base64::new(b"hello".to_vec()),
239            }),
240        };
241
242        let json_data = json!({
243            "method": "m.sas.v1",
244            "commitment": "aGVsbG8",
245            "key_agreement_protocol": "curve25519",
246            "hash": "sha256",
247            "message_authentication_code": "hkdf-hmac-sha256.v2",
248            "short_authentication_string": ["decimal"],
249            "m.relates_to": {
250                "rel_type": "m.reference",
251                "event_id": event_id,
252            }
253        });
254
255        assert_eq!(to_json_value(&key_verification_accept_content).unwrap(), json_data);
256    }
257
258    #[test]
259    fn deserialization() {
260        let json = json!({
261            "transaction_id": "456",
262            "commitment": "aGVsbG8",
263            "method": "m.sas.v1",
264            "hash": "sha256",
265            "key_agreement_protocol": "curve25519",
266            "message_authentication_code": "hkdf-hmac-sha256.v2",
267            "short_authentication_string": ["decimal"]
268        });
269
270        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
271        let content = from_json_value::<ToDeviceKeyVerificationAcceptEventContent>(json).unwrap();
272        assert_eq!(content.transaction_id, "456");
273
274        assert_matches!(content.method, AcceptMethod::SasV1(sas));
275        assert_eq!(sas.commitment.encode(), "aGVsbG8");
276        assert_eq!(sas.hash, HashAlgorithm::Sha256);
277        assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519);
278        assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
279        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
280
281        let json = json!({
282            "content": {
283                "commitment": "aGVsbG8",
284                "transaction_id": "456",
285                "method": "m.sas.v1",
286                "key_agreement_protocol": "curve25519",
287                "hash": "sha256",
288                "message_authentication_code": "hkdf-hmac-sha256.v2",
289                "short_authentication_string": ["decimal"]
290            },
291            "type": "m.key.verification.accept",
292            "sender": "@example:localhost",
293        });
294
295        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationAcceptEventContent>>(json)
296            .unwrap();
297        assert_eq!(ev.content.transaction_id, "456");
298        assert_eq!(ev.sender, "@example:localhost");
299
300        assert_matches!(ev.content.method, AcceptMethod::SasV1(sas));
301        assert_eq!(sas.commitment.encode(), "aGVsbG8");
302        assert_eq!(sas.hash, HashAlgorithm::Sha256);
303        assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519);
304        assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
305        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
306
307        let json = json!({
308            "content": {
309                "from_device": "123",
310                "transaction_id": "456",
311                "method": "m.sas.custom",
312                "test": "field",
313            },
314            "type": "m.key.verification.accept",
315            "sender": "@example:localhost",
316        });
317
318        let ev = from_json_value::<ToDeviceEvent<ToDeviceKeyVerificationAcceptEventContent>>(json)
319            .unwrap();
320        assert_eq!(ev.content.transaction_id, "456");
321        assert_eq!(ev.sender, "@example:localhost");
322
323        assert_matches!(ev.content.method, AcceptMethod::_Custom(custom));
324        assert_eq!(custom.method, "m.sas.custom");
325        assert_eq!(custom.data.get("test"), Some(&JsonValue::from("field")));
326    }
327
328    #[test]
329    fn in_room_deserialization() {
330        let json = json!({
331            "commitment": "aGVsbG8",
332            "method": "m.sas.v1",
333            "hash": "sha256",
334            "key_agreement_protocol": "curve25519",
335            "message_authentication_code": "hkdf-hmac-sha256.v2",
336            "short_authentication_string": ["decimal"],
337            "m.relates_to": {
338                "rel_type": "m.reference",
339                "event_id": "$1598361704261elfgc:localhost",
340            }
341        });
342
343        // Deserialize the content struct separately to verify `TryFromRaw` is implemented for it.
344        let content = from_json_value::<KeyVerificationAcceptEventContent>(json).unwrap();
345        assert_eq!(content.relates_to.event_id, "$1598361704261elfgc:localhost");
346
347        assert_matches!(content.method, AcceptMethod::SasV1(sas));
348        assert_eq!(sas.commitment.encode(), "aGVsbG8");
349        assert_eq!(sas.hash, HashAlgorithm::Sha256);
350        assert_eq!(sas.key_agreement_protocol, KeyAgreementProtocol::Curve25519);
351        assert_eq!(sas.message_authentication_code, MessageAuthenticationCode::HkdfHmacSha256V2);
352        assert_eq!(sas.short_authentication_string, vec![ShortAuthenticationString::Decimal]);
353    }
354
355    #[test]
356    fn in_room_serialization_roundtrip() {
357        let event_id = event_id!("$1598361704261elfgc:localhost");
358
359        let content = KeyVerificationAcceptEventContent {
360            relates_to: Reference { event_id: event_id.to_owned() },
361            method: AcceptMethod::SasV1(SasV1Content {
362                hash: HashAlgorithm::Sha256,
363                key_agreement_protocol: KeyAgreementProtocol::Curve25519,
364                message_authentication_code: MessageAuthenticationCode::HkdfHmacSha256V2,
365                short_authentication_string: vec![ShortAuthenticationString::Decimal],
366                commitment: Base64::new(b"hello".to_vec()),
367            }),
368        };
369
370        let json_content = Raw::new(&content).unwrap();
371        let deser_content = json_content.deserialize().unwrap();
372
373        assert_matches!(deser_content.method, AcceptMethod::SasV1(_));
374        assert_eq!(deser_content.relates_to.event_id, event_id);
375    }
376}