1use std::sync::Arc;
45
46use ruma::{
47 api::client::dehydrated_device::{put_dehydrated_device, DehydratedDeviceData},
48 assign,
49 events::AnyToDeviceEvent,
50 serde::Raw,
51 DeviceId,
52};
53use thiserror::Error;
54use tracing::{instrument, trace};
55use vodozemac::{DehydratedDeviceError, LibolmPickleError};
56
57use crate::{
58 store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store},
59 verification::VerificationMachine,
60 Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError,
61};
62
63#[derive(Debug, Error)]
65pub enum DehydrationError {
66 #[error(transparent)]
68 LegacyPickle(#[from] LibolmPickleError),
69
70 #[error(transparent)]
72 Pickle(#[from] DehydratedDeviceError),
73
74 #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
76 PickleKeyLength(usize),
77
78 #[error("The self-signing key is missing, can't create a dehydrated device")]
81 MissingSigningKey(#[from] SignatureError),
82
83 #[error(transparent)]
85 Json(#[from] serde_json::Error),
86
87 #[error(transparent)]
89 Store(#[from] CryptoStoreError),
90}
91
92#[derive(Debug)]
94pub struct DehydratedDevices {
95 pub(crate) inner: OlmMachine,
96}
97
98impl DehydratedDevices {
99 pub async fn create(&self) -> Result<DehydratedDevice, DehydrationError> {
101 let user_id = self.inner.user_id();
102 let user_identity = self.inner.store().private_identity();
103
104 let account = Account::new_dehydrated(user_id);
105 let store =
106 Arc::new(CryptoStoreWrapper::new(user_id, account.device_id(), MemoryStore::new()));
107
108 let verification_machine = VerificationMachine::new(
109 account.static_data().clone(),
110 user_identity.clone(),
111 store.clone(),
112 );
113
114 let store =
115 Store::new(account.static_data().clone(), user_identity, store, verification_machine);
116 store.save_pending_changes(crate::store::PendingChanges { account: Some(account) }).await?;
117
118 Ok(DehydratedDevice { store })
119 }
120
121 pub async fn rehydrate(
140 &self,
141 pickle_key: &DehydratedDeviceKey,
142 device_id: &DeviceId,
143 device_data: Raw<DehydratedDeviceData>,
144 ) -> Result<RehydratedDevice, DehydrationError> {
145 let rehydrated =
146 self.inner.rehydrate(pickle_key.inner.as_ref(), device_id, device_data).await?;
147
148 Ok(RehydratedDevice { rehydrated, original: self.inner.to_owned() })
149 }
150
151 pub async fn get_dehydrated_device_pickle_key(
159 &self,
160 ) -> Result<Option<DehydratedDeviceKey>, DehydrationError> {
161 Ok(self.inner.store().load_dehydrated_device_pickle_key().await?)
162 }
163
164 pub async fn save_dehydrated_device_pickle_key(
170 &self,
171 dehydrated_device_pickle_key: &DehydratedDeviceKey,
172 ) -> Result<(), DehydrationError> {
173 let changes = Changes {
174 dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key.clone()),
175 ..Default::default()
176 };
177 Ok(self.inner.store().save_changes(changes).await?)
178 }
179
180 pub async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), DehydrationError> {
182 Ok(self.inner.store().delete_dehydrated_device_pickle_key().await?)
183 }
184}
185
186#[derive(Debug)]
191pub struct RehydratedDevice {
192 rehydrated: OlmMachine,
193 original: OlmMachine,
194}
195
196impl RehydratedDevice {
197 #[instrument(
261 skip_all,
262 fields(
263 user_id = ?self.original.user_id(),
264 rehydrated_device_id = ?self.rehydrated.device_id(),
265 original_device_id = ?self.original.device_id()
266 )
267 )]
268 pub async fn receive_events(
269 &self,
270 events: Vec<Raw<AnyToDeviceEvent>>,
271 ) -> Result<Vec<RoomKeyInfo>, OlmError> {
272 trace!("Receiving events for a rehydrated Device");
273
274 let sync_changes = EncryptionSyncChanges {
275 to_device_events: events,
276 next_batch_token: None,
277 one_time_keys_counts: &Default::default(),
278 changed_devices: &Default::default(),
279 unused_fallback_keys: None,
280 };
281
282 let mut rehydrated_transaction = self.rehydrated.store().transaction().await;
285
286 let (_, changes) = self
287 .rehydrated
288 .preprocess_sync_changes(&mut rehydrated_transaction, sync_changes)
289 .await?;
290
291 let room_keys = &changes.inbound_group_sessions;
293 let updates = room_keys.iter().map(Into::into).collect();
294
295 trace!(room_key_count = room_keys.len(), "Collected room keys from the rehydrated device");
296
297 self.original.store().save_inbound_group_sessions(room_keys).await?;
298
299 rehydrated_transaction.commit().await?;
300 self.rehydrated.store().save_changes(changes).await?;
301
302 Ok(updates)
303 }
304}
305
306#[derive(Debug)]
311pub struct DehydratedDevice {
312 store: Store,
313}
314
315impl DehydratedDevice {
316 #[instrument(
353 skip_all, fields(
354 user_id = ?self.store.static_account().user_id,
355 device_id = ?self.store.static_account().device_id,
356 identity_keys = ?self.store.static_account().identity_keys,
357 )
358 )]
359 pub async fn keys_for_upload(
360 &self,
361 initial_device_display_name: String,
362 pickle_key: &DehydratedDeviceKey,
363 ) -> Result<put_dehydrated_device::unstable::Request, DehydrationError> {
364 let mut transaction = self.store.transaction().await;
365
366 let account = transaction.account().await?;
367 account.generate_fallback_key_if_needed();
368
369 let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
370
371 let mut device_keys = device_keys
372 .expect("We should always try to upload device keys for a dehydrated device.");
373
374 self.store.private_identity().lock().await.sign_device_keys(&mut device_keys).await?;
375
376 trace!("Creating an upload request for a dehydrated device");
377
378 let device_id = self.store.static_account().device_id.clone();
379 let device_data = account.dehydrate(pickle_key.inner.as_ref());
380 let initial_device_display_name = Some(initial_device_display_name);
381
382 transaction.commit().await?;
383
384 Ok(
385 assign!(put_dehydrated_device::unstable::Request::new(device_id, device_data, device_keys.to_raw()), {
386 one_time_keys, fallback_keys, initial_device_display_name
387 }),
388 )
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use std::{collections::BTreeMap, iter};
395
396 use js_option::JsOption;
397 use matrix_sdk_test::async_test;
398 use ruma::{
399 api::client::{
400 dehydrated_device::put_dehydrated_device,
401 keys::get_keys::v3::Response as KeysQueryResponse,
402 },
403 assign,
404 encryption::DeviceKeys,
405 events::AnyToDeviceEvent,
406 room_id,
407 serde::Raw,
408 user_id, DeviceId, RoomId, TransactionId, UserId,
409 };
410
411 use crate::{
412 dehydrated_devices::DehydratedDevice,
413 machine::{
414 test_helpers::{create_session, get_prepared_machine_test_helper},
415 tests::to_device_requests_to_content,
416 },
417 olm::OutboundGroupSession,
418 store::DehydratedDeviceKey,
419 types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
420 utilities::json_convert,
421 EncryptionSettings, OlmMachine,
422 };
423
424 fn pickle_key() -> DehydratedDeviceKey {
425 DehydratedDeviceKey::from_bytes(&[0u8; 32])
426 }
427
428 fn user_id() -> &'static UserId {
429 user_id!("@alice:localhost")
430 }
431
432 async fn get_olm_machine() -> OlmMachine {
433 let (olm_machine, _) = get_prepared_machine_test_helper(user_id(), false).await;
434 olm_machine.bootstrap_cross_signing(false).await.unwrap();
435
436 olm_machine
437 }
438
439 async fn receive_device_keys(
442 olm_machine: &OlmMachine,
443 user_id: &UserId,
444 device_id: &DeviceId,
445 device_keys: Raw<DeviceKeys>,
446 ) {
447 let device_keys = BTreeMap::from([(device_id.to_owned(), device_keys)]);
448
449 let keys_query_response = assign!(
450 KeysQueryResponse::new(), {
451 device_keys: BTreeMap::from([(user_id.to_owned(), device_keys)]),
452 }
453 );
454
455 olm_machine
456 .mark_request_as_sent(&TransactionId::new(), &keys_query_response)
457 .await
458 .unwrap();
459 }
460
461 async fn send_room_key(
462 machine: &OlmMachine,
463 room_id: &RoomId,
464 recipient: &UserId,
465 ) -> (Raw<AnyToDeviceEvent>, OutboundGroupSession) {
466 let to_device_requests = machine
467 .share_room_key(room_id, iter::once(recipient), EncryptionSettings::default())
468 .await
469 .unwrap();
470
471 let event = ToDeviceEvent::new(
472 user_id().to_owned(),
473 to_device_requests_to_content(to_device_requests),
474 );
475
476 let session =
477 machine.inner.group_session_manager.get_outbound_group_session(room_id).expect(
478 "An outbound group session should have been created when the room key was shared",
479 );
480
481 (
482 json_convert(&event)
483 .expect("We should be able to convert the to-device event into it's Raw variatn"),
484 session,
485 )
486 }
487
488 #[async_test]
489 async fn test_dehydrated_device_creation() {
490 let olm_machine = get_olm_machine().await;
491
492 let dehydrated_device = olm_machine.dehydrated_devices().create().await.unwrap();
493
494 let request = dehydrated_device
495 .keys_for_upload("Foo".to_owned(), &pickle_key())
496 .await
497 .expect("We should be able to create a request to upload a dehydrated device");
498
499 assert!(
500 !request.one_time_keys.is_empty(),
501 "The dehydrated device creation request should contain some one-time keys"
502 );
503
504 assert!(
505 !request.fallback_keys.is_empty(),
506 "The dehydrated device creation request should contain some fallback keys"
507 );
508
509 let device_keys: DeviceKeysType = request.device_keys.deserialize_as().unwrap();
510 assert_eq!(
511 device_keys.dehydrated,
512 JsOption::Some(true),
513 "The device keys of the dehydrated device should be marked as dehydrated."
514 );
515 }
516
517 #[async_test]
518 async fn test_dehydrated_device_rehydration() {
519 let room_id = room_id!("!test:example.org");
520 let alice = get_olm_machine().await;
521
522 let dehydrated_device = alice.dehydrated_devices().create().await.unwrap();
523
524 let mut request = dehydrated_device
525 .keys_for_upload("Foo".to_owned(), &pickle_key())
526 .await
527 .expect("We should be able to create a request to upload a dehydrated device");
528
529 let (key_id, one_time_key) = request
530 .one_time_keys
531 .pop_first()
532 .expect("The dehydrated device creation request should contain a one-time key");
533
534 receive_device_keys(&alice, user_id(), &request.device_id, request.device_keys).await;
536 create_session(&alice, user_id(), &request.device_id, key_id, one_time_key).await;
538
539 let (event, group_session) = send_room_key(&alice, room_id, user_id()).await;
541
542 let bob = get_olm_machine().await;
544
545 let room_key = bob
546 .store()
547 .get_inbound_group_session(room_id, group_session.session_id())
548 .await
549 .unwrap();
550
551 assert!(
552 room_key.is_none(),
553 "We should not have access to the room key that was only sent to the dehydrated device"
554 );
555
556 let rehydrated = bob
558 .dehydrated_devices()
559 .rehydrate(&pickle_key(), &request.device_id, request.device_data)
560 .await
561 .expect("We should be able to rehydrate the device");
562
563 assert_eq!(rehydrated.rehydrated.device_id(), request.device_id);
564 assert_eq!(rehydrated.original.device_id(), alice.device_id());
565
566 let ret = rehydrated
568 .receive_events(vec![event])
569 .await
570 .expect("We should be able to push to-device events into the rehydrated device");
571
572 assert_eq!(ret.len(), 1, "The rehydrated device should have imported a room key");
573
574 let room_key = bob
577 .store()
578 .get_inbound_group_session(room_id, group_session.session_id())
579 .await
580 .unwrap()
581 .expect("We should now have access to the room key, since the rehydrated device imported it for us");
582
583 assert_eq!(
584 room_key.session_id(),
585 group_session.session_id(),
586 "The session ids of the imported room key and the outbound group session should match"
587 );
588 }
589
590 #[async_test]
591 async fn test_dehydrated_device_pickle_key_cache() {
592 let alice = get_olm_machine().await;
593
594 let dehydrated_manager = alice.dehydrated_devices();
595
596 let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
597 assert!(stored_key.is_none());
598
599 let pickle_key = DehydratedDeviceKey::new().unwrap();
600
601 dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
602
603 let stored_key =
604 dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap().unwrap();
605 assert_eq!(stored_key.to_base64(), pickle_key.to_base64());
606
607 let dehydrated_device = dehydrated_manager.create().await.unwrap();
608
609 let request = dehydrated_device
610 .keys_for_upload("Foo".to_owned(), &stored_key)
611 .await
612 .expect("We should be able to create a request to upload a dehydrated device");
613
614 dehydrated_manager
616 .rehydrate(&stored_key, &request.device_id, request.device_data)
617 .await
618 .expect("We should be able to rehydrate the device");
619
620 dehydrated_manager
621 .delete_dehydrated_device_pickle_key()
622 .await
623 .expect("Should be able to delete the dehydrated device key");
624
625 let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
626 assert!(stored_key.is_none());
627 }
628
629 #[async_test]
631 async fn test_legacy_dehydrated_device_rehydration() {
632 let room_id = room_id!("!test:example.org");
633 let alice = get_olm_machine().await;
634
635 let dehydrated_device = alice.dehydrated_devices().create().await.unwrap();
636 let mut request =
637 legacy_dehydrated_device_keys_for_upload(&dehydrated_device, &pickle_key()).await;
638
639 let (key_id, one_time_key) = request
640 .one_time_keys
641 .pop_first()
642 .expect("The dehydrated device creation request should contain a one-time key");
643
644 let device_id = request.device_id;
645
646 receive_device_keys(&alice, user_id(), &device_id, request.device_keys).await;
648 create_session(&alice, user_id(), &device_id, key_id, one_time_key).await;
650
651 let (event, group_session) = send_room_key(&alice, room_id, user_id()).await;
653
654 let bob = get_olm_machine().await;
656
657 let room_key = bob
658 .store()
659 .get_inbound_group_session(room_id, group_session.session_id())
660 .await
661 .unwrap();
662
663 assert!(
664 room_key.is_none(),
665 "We should not have access to the room key that was only sent to the dehydrated device"
666 );
667
668 let rehydrated = bob
670 .dehydrated_devices()
671 .rehydrate(&pickle_key(), &device_id, request.device_data)
672 .await
673 .expect("We should be able to rehydrate the device");
674
675 assert_eq!(rehydrated.rehydrated.device_id(), &device_id);
676 assert_eq!(rehydrated.original.device_id(), alice.device_id());
677
678 let ret = rehydrated
680 .receive_events(vec![event])
681 .await
682 .expect("We should be able to push to-device events into the rehydrated device");
683
684 assert_eq!(ret.len(), 1, "The rehydrated device should have imported a room key");
685
686 let room_key = bob
689 .store()
690 .get_inbound_group_session(room_id, group_session.session_id())
691 .await
692 .unwrap()
693 .expect("We should now have access to the room key, since the rehydrated device imported it for us");
694
695 assert_eq!(
696 room_key.session_id(),
697 group_session.session_id(),
698 "The session ids of the imported room key and the outbound group session should match"
699 );
700 }
701
702 async fn legacy_dehydrated_device_keys_for_upload(
706 dehydrated_device: &DehydratedDevice,
707 pickle_key: &DehydratedDeviceKey,
708 ) -> put_dehydrated_device::unstable::Request {
709 let mut transaction = dehydrated_device.store.transaction().await;
710 let account = transaction.account().await.unwrap();
711 account.generate_fallback_key_if_needed();
712
713 let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
714 let mut device_keys = device_keys.unwrap();
715 dehydrated_device
716 .store
717 .private_identity()
718 .lock()
719 .await
720 .sign_device_keys(&mut device_keys)
721 .await
722 .expect("Should be able to cross-sign a device");
723
724 let device_id = account.device_id().to_owned();
725 let device_data = account.legacy_dehydrate(pickle_key.inner.as_ref());
726 transaction.commit().await.unwrap();
727
728 assign!(put_dehydrated_device::unstable::Request::new(device_id, device_data, device_keys.to_raw()), {
729 one_time_keys, fallback_keys
730 })
731 }
732}