1use 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#[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 pub from_device: OwnedDeviceId,
26
27 pub transaction_id: OwnedTransactionId,
33
34 #[serde(flatten)]
36 pub method: StartMethod,
37}
38
39impl ToDeviceKeyVerificationStartEventContent {
40 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#[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 pub from_device: OwnedDeviceId,
60
61 #[serde(flatten)]
63 pub method: StartMethod,
64
65 #[serde(rename = "m.relates_to")]
67 pub relates_to: Reference,
68}
69
70impl KeyVerificationStartEventContent {
71 pub fn new(from_device: OwnedDeviceId, method: StartMethod, relates_to: Reference) -> Self {
74 Self { from_device, method, relates_to }
75 }
76}
77
78#[derive(Clone, Debug, Deserialize, Serialize)]
80#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
81#[serde(untagged)]
82pub enum StartMethod {
83 SasV1(SasV1Content),
85
86 ReciprocateV1(ReciprocateV1Content),
92
93 #[doc(hidden)]
95 _Custom(_CustomContent),
96}
97
98#[doc(hidden)]
100#[derive(Clone, Debug, Deserialize, Serialize)]
101#[allow(clippy::exhaustive_structs)]
102pub struct _CustomContent {
103 pub method: String,
105
106 #[serde(flatten)]
108 pub data: BTreeMap<String, JsonValue>,
109}
110
111#[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 pub secret: Base64,
118}
119
120impl ReciprocateV1Content {
121 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#[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 pub key_agreement_protocols: Vec<KeyAgreementProtocol>,
147
148 pub hashes: Vec<HashAlgorithm>,
152
153 pub message_authentication_codes: Vec<MessageAuthenticationCode>,
159
160 pub short_authentication_string: Vec<ShortAuthenticationString>,
164}
165
166#[derive(Debug)]
171#[allow(clippy::exhaustive_structs)]
172pub struct SasV1ContentInit {
173 pub key_agreement_protocols: Vec<KeyAgreementProtocol>,
177
178 pub hashes: Vec<HashAlgorithm>,
182
183 pub message_authentication_codes: Vec<MessageAuthenticationCode>,
189
190 pub short_authentication_string: Vec<ShortAuthenticationString>,
194}
195
196impl From<SasV1ContentInit> for SasV1Content {
197 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 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 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}