1use 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#[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 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 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 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}