matrix_sdk_crypto/
dehydrated_devices.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Submodule for device dehydration support.
16//!
17//! Dehydrated devices intend to solve the use-case where users might want to
18//! frequently delete their device, in which case other users won't be able to
19//! send end-to-end encrypted messages to them as no device exists to receive
20//! and decrypt them.
21//!
22//! A dehydrated device is a kind-of omnipresent virtual device that lives on
23//! the homeserver. A dehydrated device acts as a normal device from the
24//! point of view of other devices. It uploads device and one-time keys to
25//! the homeserver which other devices can download and start 1-to-1 encrypted
26//! sessions with the device just like with any other device.
27//!
28//! The one important difference is that the private parts of the uploaded
29//! device and one-time keys are encrypted and uploaded to the homeserver as
30//! well.
31//!
32//! Once the user creates a new real device, the real device can download the
33//! private keys of the dehydrated device from the homeserver, decrypt them and
34//! download all the encrypted to-device events the dehydrated device has
35//! received. This process is called rehydration.
36//!
37//! After the rehydration process is completed, the user's real device should
38//! create a new dehydrated device.
39
40// TODO: Once a device has been rehydrated it might need to download and decrypt
41// a lot of to-device events. This process might take some time and we should
42// support resuming it.
43
44use std::sync::Arc;
45
46use ruma::{
47    DeviceId,
48    api::client::dehydrated_device::{DehydratedDeviceData, put_dehydrated_device},
49    assign,
50    events::AnyToDeviceEvent,
51    serde::Raw,
52};
53use thiserror::Error;
54use tracing::{instrument, trace};
55use vodozemac::{DehydratedDeviceError, LibolmPickleError};
56
57use crate::{
58    Account, CryptoStoreError, DecryptionSettings, EncryptionSyncChanges, OlmError, OlmMachine,
59    SignatureError,
60    store::{
61        CryptoStoreWrapper, MemoryStore, Store,
62        types::{Changes, DehydratedDeviceKey, RoomKeyInfo},
63    },
64    verification::VerificationMachine,
65};
66
67/// Error type for device dehydration issues.
68#[derive(Debug, Error)]
69pub enum DehydrationError {
70    /// The legacy dehydrated device could not be unpickled.
71    #[error(transparent)]
72    LegacyPickle(#[from] LibolmPickleError),
73
74    /// The dehydrated device could not be unpickled.
75    #[error(transparent)]
76    Pickle(#[from] DehydratedDeviceError),
77
78    /// The pickle key has an invalid length
79    #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
80    PickleKeyLength(usize),
81
82    /// The dehydrated device could not be signed by our user identity,
83    /// we're missing the self-signing key.
84    #[error("The self-signing key is missing, can't create a dehydrated device")]
85    MissingSigningKey(#[from] SignatureError),
86
87    /// We could not deserialize the dehydrated device data.
88    #[error(transparent)]
89    Json(#[from] serde_json::Error),
90
91    /// The store ran into an error.
92    #[error(transparent)]
93    Store(#[from] CryptoStoreError),
94}
95
96/// Struct collecting methods to create and rehydrate dehydrated devices.
97#[derive(Debug)]
98pub struct DehydratedDevices {
99    pub(crate) inner: OlmMachine,
100}
101
102impl DehydratedDevices {
103    /// Create a new [`DehydratedDevice`] which can be uploaded to the server.
104    pub async fn create(&self) -> Result<DehydratedDevice, DehydrationError> {
105        let user_id = self.inner.user_id();
106        let user_identity = self.inner.store().private_identity();
107
108        let account = Account::new_dehydrated(user_id);
109        let store =
110            Arc::new(CryptoStoreWrapper::new(user_id, account.device_id(), MemoryStore::new()));
111
112        let verification_machine = VerificationMachine::new(
113            account.static_data().clone(),
114            user_identity.clone(),
115            store.clone(),
116        );
117
118        let store =
119            Store::new(account.static_data().clone(), user_identity, store, verification_machine);
120        store
121            .save_pending_changes(crate::store::types::PendingChanges { account: Some(account) })
122            .await?;
123
124        Ok(DehydratedDevice { store })
125    }
126
127    /// Rehydrate the dehydrated device.
128    ///
129    /// Once rehydrated, to-device events can be pushed into the
130    /// [`RehydratedDevice`] to collect the room keys the device has
131    /// received.
132    ///
133    /// For more info see the example for the
134    /// [`RehydratedDevice::receive_events()`] method.
135    ///
136    /// # Arguments
137    ///
138    /// * `pickle_key` - The encryption key that was used to encrypt the private
139    ///   parts of the identity keys, and one-time keys of the device.
140    ///
141    /// * `device_id` - The unique identifier of the device.
142    ///
143    /// * `device_data` - The encrypted data of the device, containing the
144    ///   private keys of the device.
145    pub async fn rehydrate(
146        &self,
147        pickle_key: &DehydratedDeviceKey,
148        device_id: &DeviceId,
149        device_data: Raw<DehydratedDeviceData>,
150    ) -> Result<RehydratedDevice, DehydrationError> {
151        let rehydrated =
152            self.inner.rehydrate(pickle_key.inner.as_ref(), device_id, device_data).await?;
153
154        Ok(RehydratedDevice { rehydrated, original: self.inner.to_owned() })
155    }
156
157    /// Get the cached dehydrated device pickle key if any.
158    ///
159    /// None if the key was not previously cached (via
160    /// [`DehydratedDevices::save_dehydrated_device_pickle_key`]).
161    ///
162    /// Should be used to periodically rotate the dehydrated device to avoid
163    /// one-time keys exhaustion and accumulation of to_device messages.
164    pub async fn get_dehydrated_device_pickle_key(
165        &self,
166    ) -> Result<Option<DehydratedDeviceKey>, DehydrationError> {
167        Ok(self.inner.store().load_dehydrated_device_pickle_key().await?)
168    }
169
170    /// Store the dehydrated device pickle key in the crypto store.
171    ///
172    /// This is useful if the client wants to periodically rotate dehydrated
173    /// devices to avoid one-time keys exhaustion and accumulated to_device
174    /// problems.
175    pub async fn save_dehydrated_device_pickle_key(
176        &self,
177        dehydrated_device_pickle_key: &DehydratedDeviceKey,
178    ) -> Result<(), DehydrationError> {
179        let changes = Changes {
180            dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key.clone()),
181            ..Default::default()
182        };
183        Ok(self.inner.store().save_changes(changes).await?)
184    }
185
186    /// Deletes the previously stored dehydrated device pickle key.
187    pub async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), DehydrationError> {
188        Ok(self.inner.store().delete_dehydrated_device_pickle_key().await?)
189    }
190}
191
192/// A rehydraded device.
193///
194/// This device can now receive to-device events to decrypt and gather room keys
195/// which were sent to the dehydrated device.
196#[derive(Debug)]
197pub struct RehydratedDevice {
198    rehydrated: OlmMachine,
199    original: OlmMachine,
200}
201
202impl RehydratedDevice {
203    /// Feed to-device events the device was supposed to receive into the
204    /// [`RehydratedDevice`].
205    ///
206    /// Most to-device events we feed into the [`RehydratedDevice`] will contain
207    /// room keys, the rehydrated device will pass these room keys into our
208    /// own [`OlmMachine`] which will persist them and make the room keys
209    /// available for use using the usual
210    /// [`OlmMachine::decrypt_room_event()`] method.
211    ///
212    /// Once the homeserver returns a response without any to-device events, we
213    /// can safely delete the current dehydrated device and create a new one.
214    ///
215    /// # Examples
216    ///
217    /// ```no_run
218    /// # use anyhow::Result;
219    /// # use matrix_sdk_crypto::{
220    ///     DecryptionSettings, OlmMachine, TrustRequirement, store::types::DehydratedDeviceKey
221    /// };
222    /// # use ruma::{api::client::dehydrated_device, DeviceId};
223    /// # async fn example() -> Result<()> {
224    /// # let machine: OlmMachine = unimplemented!();
225    /// async fn get_dehydrated_device() -> Result<dehydrated_device::get_dehydrated_device::unstable::Response> {
226    ///     todo!("Download the dehydrated device");
227    /// }
228    ///
229    /// async fn get_events(
230    ///     device_id: &DeviceId,
231    ///     since_token: Option<&str>
232    /// ) -> Result<dehydrated_device::get_events::unstable::Response> {
233    ///     todo!("Download the to-device events of the dehydrated device");
234    /// }
235    /// // Get the cached dehydrated key (got it after verification/recovery)
236    /// let pickle_key = machine
237    ///     .dehydrated_devices().get_dehydrated_device_pickle_key().await?.unwrap();
238    ///
239    /// // Fetch the dehydrated device from the server.
240    /// let response = get_dehydrated_device().await?;
241    /// let device_id = response.device_id;
242    ///
243    /// // Rehydrate the device.
244    /// let rehydrated = machine
245    ///     .dehydrated_devices()
246    ///     .rehydrate(&pickle_key, &device_id, response.device_data)
247    ///     .await?;
248    ///
249    /// let mut since_token = None;
250    /// let mut imported_room_keys = 0;
251    /// let decryption_settings = DecryptionSettings {
252    ///     sender_device_trust_requirement: TrustRequirement::Untrusted
253    /// };
254    ///
255    /// loop {
256    ///     let response =
257    ///         get_events(&device_id, since_token).await?;
258    ///
259    ///     if response.events.is_empty() {
260    ///         break;
261    ///     }
262    ///
263    ///     since_token = response.next_batch.as_deref();
264    ///     imported_room_keys += rehydrated.receive_events(response.events, &decryption_settings).await?.len();
265    /// }
266    ///
267    /// println!("Successfully imported {imported_room_keys} from the dehydrated device.");
268    /// # Ok(())
269    /// # }
270    /// ```
271    #[instrument(
272        skip_all,
273        fields(
274            user_id = ?self.original.user_id(),
275            rehydrated_device_id = ?self.rehydrated.device_id(),
276            original_device_id = ?self.original.device_id()
277        )
278    )]
279    pub async fn receive_events(
280        &self,
281        events: Vec<Raw<AnyToDeviceEvent>>,
282        decryption_settings: &DecryptionSettings,
283    ) -> Result<Vec<RoomKeyInfo>, OlmError> {
284        trace!("Receiving events for a rehydrated Device");
285
286        let sync_changes = EncryptionSyncChanges {
287            to_device_events: events,
288            next_batch_token: None,
289            one_time_keys_counts: &Default::default(),
290            changed_devices: &Default::default(),
291            unused_fallback_keys: None,
292        };
293
294        // Let us first give the events to the rehydrated device, this will decrypt any
295        // encrypted to-device events and fetch out the room keys.
296        let mut rehydrated_transaction = self.rehydrated.store().transaction().await;
297
298        let (_, changes) = self
299            .rehydrated
300            .preprocess_sync_changes(&mut rehydrated_transaction, sync_changes, decryption_settings)
301            .await?;
302
303        // Now take the room keys and persist them in our original `OlmMachine`.
304        let room_keys = &changes.inbound_group_sessions;
305        let updates = room_keys.iter().map(Into::into).collect();
306
307        trace!(room_key_count = room_keys.len(), "Collected room keys from the rehydrated device");
308
309        self.original.store().save_inbound_group_sessions(room_keys).await?;
310
311        rehydrated_transaction.commit().await?;
312        self.rehydrated.store().save_changes(changes).await?;
313
314        Ok(updates)
315    }
316}
317
318/// A dehydrated device that can uploaded to the homeserver.
319///
320/// To upload the dehydrated device take a look at the
321/// [`DehydratedDevice::keys_for_upload()`] method.
322#[derive(Debug)]
323pub struct DehydratedDevice {
324    store: Store,
325}
326
327impl DehydratedDevice {
328    /// Get the request to upload the dehydrated device.
329    ///
330    /// # Arguments
331    ///
332    /// * `initial_device_display_name` - The human-readable name this device
333    ///   should have.
334    /// * `pickle_key` - The encryption key that should be used to encrypt the
335    ///   private parts of the identity keys, and one-time keys of the device.
336    ///
337    /// # Examples
338    ///
339    /// ```no_run
340    /// # use matrix_sdk_crypto::OlmMachine;    /// #
341    /// use matrix_sdk_crypto::store::types::DehydratedDeviceKey;
342    ///
343    /// async fn example() -> anyhow::Result<()> {
344    /// # let machine: OlmMachine = unimplemented!();
345    /// // Create a new random key
346    /// let pickle_key = DehydratedDeviceKey::new()?;
347    ///
348    /// // Create the dehydrated device.
349    /// let device = machine.dehydrated_devices().create().await?;
350    ///
351    /// // Create the request that should upload the device.
352    /// let request = device
353    ///     .keys_for_upload("Dehydrated device".to_owned(), &pickle_key)
354    ///     .await?;
355    ///
356    /// // Save the key if you want to later one rotate the dehydrated device
357    /// machine.dehydrated_devices().save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
358    ///
359    /// // Send the request out using your HTTP client.
360    /// // client.send(request).await?;
361    /// # Ok(())
362    /// # }
363    /// ```
364    #[instrument(
365        skip_all, fields(
366            user_id = ?self.store.static_account().user_id,
367            device_id = ?self.store.static_account().device_id,
368            identity_keys = ?self.store.static_account().identity_keys,
369        )
370    )]
371    pub async fn keys_for_upload(
372        &self,
373        initial_device_display_name: String,
374        pickle_key: &DehydratedDeviceKey,
375    ) -> Result<put_dehydrated_device::unstable::Request, DehydrationError> {
376        let mut transaction = self.store.transaction().await;
377
378        let account = transaction.account().await?;
379        account.generate_fallback_key_if_needed();
380
381        let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
382
383        let mut device_keys = device_keys
384            .expect("We should always try to upload device keys for a dehydrated device.");
385
386        self.store.private_identity().lock().await.sign_device_keys(&mut device_keys).await?;
387
388        trace!("Creating an upload request for a dehydrated device");
389
390        let device_id = self.store.static_account().device_id.clone();
391        let device_data = account.dehydrate(pickle_key.inner.as_ref());
392        let initial_device_display_name = Some(initial_device_display_name);
393
394        transaction.commit().await?;
395
396        Ok(
397            assign!(put_dehydrated_device::unstable::Request::new(device_id, device_data, device_keys.to_raw()), {
398                one_time_keys, fallback_keys, initial_device_display_name
399            }),
400        )
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use std::{collections::BTreeMap, iter};
407
408    use js_option::JsOption;
409    use matrix_sdk_test::async_test;
410    use ruma::{
411        DeviceId, RoomId, TransactionId, UserId,
412        api::client::{
413            dehydrated_device::put_dehydrated_device,
414            keys::get_keys::v3::Response as KeysQueryResponse,
415        },
416        assign,
417        encryption::DeviceKeys,
418        events::AnyToDeviceEvent,
419        room_id,
420        serde::Raw,
421        user_id,
422    };
423
424    use crate::{
425        DecryptionSettings, EncryptionSettings, OlmMachine, TrustRequirement,
426        dehydrated_devices::DehydratedDevice,
427        machine::{
428            test_helpers::{create_session, get_prepared_machine_test_helper},
429            tests::to_device_requests_to_content,
430        },
431        olm::OutboundGroupSession,
432        store::types::DehydratedDeviceKey,
433        types::{DeviceKeys as DeviceKeysType, events::ToDeviceEvent},
434        utilities::json_convert,
435    };
436
437    fn pickle_key() -> DehydratedDeviceKey {
438        DehydratedDeviceKey::from_bytes(&[0u8; 32])
439    }
440
441    fn user_id() -> &'static UserId {
442        user_id!("@alice:localhost")
443    }
444
445    async fn get_olm_machine() -> OlmMachine {
446        let (olm_machine, _) = get_prepared_machine_test_helper(user_id(), false).await;
447        olm_machine.bootstrap_cross_signing(false).await.unwrap();
448
449        olm_machine
450    }
451
452    // Insert some device keys into a [`OlmMachine`] making the [`Device`] available
453    // to the [`OlmMachine`].
454    async fn receive_device_keys(
455        olm_machine: &OlmMachine,
456        user_id: &UserId,
457        device_id: &DeviceId,
458        device_keys: Raw<DeviceKeys>,
459    ) {
460        let device_keys = BTreeMap::from([(device_id.to_owned(), device_keys)]);
461
462        let keys_query_response = assign!(
463            KeysQueryResponse::new(), {
464                device_keys: BTreeMap::from([(user_id.to_owned(), device_keys)]),
465            }
466        );
467
468        olm_machine
469            .mark_request_as_sent(&TransactionId::new(), &keys_query_response)
470            .await
471            .unwrap();
472    }
473
474    async fn send_room_key(
475        machine: &OlmMachine,
476        room_id: &RoomId,
477        recipient: &UserId,
478    ) -> (Raw<AnyToDeviceEvent>, OutboundGroupSession) {
479        let to_device_requests = machine
480            .share_room_key(room_id, iter::once(recipient), EncryptionSettings::default())
481            .await
482            .unwrap();
483
484        let event = ToDeviceEvent::new(
485            user_id().to_owned(),
486            to_device_requests_to_content(to_device_requests),
487        );
488
489        let session =
490            machine.inner.group_session_manager.get_outbound_group_session(room_id).expect(
491                "An outbound group session should have been created when the room key was shared",
492            );
493
494        (
495            json_convert(&event)
496                .expect("We should be able to convert the to-device event into it's Raw variatn"),
497            session,
498        )
499    }
500
501    #[async_test]
502    async fn test_dehydrated_device_creation() {
503        let olm_machine = get_olm_machine().await;
504
505        let dehydrated_device = olm_machine.dehydrated_devices().create().await.unwrap();
506
507        let request = dehydrated_device
508            .keys_for_upload("Foo".to_owned(), &pickle_key())
509            .await
510            .expect("We should be able to create a request to upload a dehydrated device");
511
512        assert!(
513            !request.one_time_keys.is_empty(),
514            "The dehydrated device creation request should contain some one-time keys"
515        );
516
517        assert!(
518            !request.fallback_keys.is_empty(),
519            "The dehydrated device creation request should contain some fallback keys"
520        );
521
522        let device_keys: DeviceKeysType = request.device_keys.deserialize_as().unwrap();
523        assert_eq!(
524            device_keys.dehydrated,
525            JsOption::Some(true),
526            "The device keys of the dehydrated device should be marked as dehydrated."
527        );
528    }
529
530    #[async_test]
531    async fn test_dehydrated_device_rehydration() {
532        let room_id = room_id!("!test:example.org");
533        let alice = get_olm_machine().await;
534
535        let dehydrated_device = alice.dehydrated_devices().create().await.unwrap();
536
537        let mut request = dehydrated_device
538            .keys_for_upload("Foo".to_owned(), &pickle_key())
539            .await
540            .expect("We should be able to create a request to upload a dehydrated device");
541
542        let (key_id, one_time_key) = request
543            .one_time_keys
544            .pop_first()
545            .expect("The dehydrated device creation request should contain a one-time key");
546
547        // Ensure that we know about the public keys of the dehydrated device.
548        receive_device_keys(&alice, user_id(), &request.device_id, request.device_keys).await;
549        // Create a 1-to-1 Olm session with the dehydrated device.
550        create_session(&alice, user_id(), &request.device_id, key_id, one_time_key).await;
551
552        // Send a room key to the dehydrated device.
553        let (event, group_session) = send_room_key(&alice, room_id, user_id()).await;
554
555        // Let's now create a new `OlmMachine` which doesn't know about the room key.
556        let bob = get_olm_machine().await;
557
558        let room_key = bob
559            .store()
560            .get_inbound_group_session(room_id, group_session.session_id())
561            .await
562            .unwrap();
563
564        assert!(
565            room_key.is_none(),
566            "We should not have access to the room key that was only sent to the dehydrated device"
567        );
568
569        // Rehydrate the device.
570        let rehydrated = bob
571            .dehydrated_devices()
572            .rehydrate(&pickle_key(), &request.device_id, request.device_data)
573            .await
574            .expect("We should be able to rehydrate the device");
575
576        assert_eq!(rehydrated.rehydrated.device_id(), request.device_id);
577        assert_eq!(rehydrated.original.device_id(), alice.device_id());
578
579        let decryption_settings =
580            DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
581
582        // Push the to-device event containing the room key into the rehydrated device.
583        let ret = rehydrated
584            .receive_events(vec![event], &decryption_settings)
585            .await
586            .expect("We should be able to push to-device events into the rehydrated device");
587
588        assert_eq!(ret.len(), 1, "The rehydrated device should have imported a room key");
589
590        // The `OlmMachine` now does know about the room key since the rehydrated device
591        // shared it with us.
592        let room_key = bob
593            .store()
594            .get_inbound_group_session(room_id, group_session.session_id())
595            .await
596            .unwrap()
597            .expect("We should now have access to the room key, since the rehydrated device imported it for us");
598
599        assert_eq!(
600            room_key.session_id(),
601            group_session.session_id(),
602            "The session ids of the imported room key and the outbound group session should match"
603        );
604    }
605
606    #[async_test]
607    async fn test_dehydrated_device_pickle_key_cache() {
608        let alice = get_olm_machine().await;
609
610        let dehydrated_manager = alice.dehydrated_devices();
611
612        let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
613        assert!(stored_key.is_none());
614
615        let pickle_key = DehydratedDeviceKey::new().unwrap();
616
617        dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
618
619        let stored_key =
620            dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap().unwrap();
621        assert_eq!(stored_key.to_base64(), pickle_key.to_base64());
622
623        let dehydrated_device = dehydrated_manager.create().await.unwrap();
624
625        let request = dehydrated_device
626            .keys_for_upload("Foo".to_owned(), &stored_key)
627            .await
628            .expect("We should be able to create a request to upload a dehydrated device");
629
630        // Rehydrate the device.
631        dehydrated_manager
632            .rehydrate(&stored_key, &request.device_id, request.device_data)
633            .await
634            .expect("We should be able to rehydrate the device");
635
636        dehydrated_manager
637            .delete_dehydrated_device_pickle_key()
638            .await
639            .expect("Should be able to delete the dehydrated device key");
640
641        let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
642        assert!(stored_key.is_none());
643    }
644
645    /// Test that we can rehydrate an older version of dehydrated device
646    #[async_test]
647    async fn test_legacy_dehydrated_device_rehydration() {
648        let room_id = room_id!("!test:example.org");
649        let alice = get_olm_machine().await;
650
651        let dehydrated_device = alice.dehydrated_devices().create().await.unwrap();
652        let mut request =
653            legacy_dehydrated_device_keys_for_upload(&dehydrated_device, &pickle_key()).await;
654
655        let (key_id, one_time_key) = request
656            .one_time_keys
657            .pop_first()
658            .expect("The dehydrated device creation request should contain a one-time key");
659
660        let device_id = request.device_id;
661
662        // Ensure that we know about the public keys of the dehydrated device.
663        receive_device_keys(&alice, user_id(), &device_id, request.device_keys).await;
664        // Create a 1-to-1 Olm session with the dehydrated device.
665        create_session(&alice, user_id(), &device_id, key_id, one_time_key).await;
666
667        // Send a room key to the dehydrated device.
668        let (event, group_session) = send_room_key(&alice, room_id, user_id()).await;
669
670        // Let's now create a new `OlmMachine` which doesn't know about the room key.
671        let bob = get_olm_machine().await;
672
673        let room_key = bob
674            .store()
675            .get_inbound_group_session(room_id, group_session.session_id())
676            .await
677            .unwrap();
678
679        assert!(
680            room_key.is_none(),
681            "We should not have access to the room key that was only sent to the dehydrated device"
682        );
683
684        // Rehydrate the device.
685        let rehydrated = bob
686            .dehydrated_devices()
687            .rehydrate(&pickle_key(), &device_id, request.device_data)
688            .await
689            .expect("We should be able to rehydrate the device");
690
691        assert_eq!(rehydrated.rehydrated.device_id(), &device_id);
692        assert_eq!(rehydrated.original.device_id(), alice.device_id());
693
694        let decryption_settings =
695            DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
696
697        // Push the to-device event containing the room key into the rehydrated device.
698        let ret = rehydrated
699            .receive_events(vec![event], &decryption_settings)
700            .await
701            .expect("We should be able to push to-device events into the rehydrated device");
702
703        assert_eq!(ret.len(), 1, "The rehydrated device should have imported a room key");
704
705        // The `OlmMachine` now does know about the room key since the rehydrated device
706        // shared it with us.
707        let room_key = bob
708            .store()
709            .get_inbound_group_session(room_id, group_session.session_id())
710            .await
711            .unwrap()
712            .expect("We should now have access to the room key, since the rehydrated device imported it for us");
713
714        assert_eq!(
715            room_key.session_id(),
716            group_session.session_id(),
717            "The session ids of the imported room key and the outbound group session should match"
718        );
719    }
720
721    /// Duplicates the behaviour of [`DehydratedDevice::keys_for_upload`],
722    /// except that it calls [`Account::legacy_dehydrate`] instead of
723    /// [`Account::dehydrate`].
724    async fn legacy_dehydrated_device_keys_for_upload(
725        dehydrated_device: &DehydratedDevice,
726        pickle_key: &DehydratedDeviceKey,
727    ) -> put_dehydrated_device::unstable::Request {
728        let mut transaction = dehydrated_device.store.transaction().await;
729        let account = transaction.account().await.unwrap();
730        account.generate_fallback_key_if_needed();
731
732        let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
733        let mut device_keys = device_keys.unwrap();
734        dehydrated_device
735            .store
736            .private_identity()
737            .lock()
738            .await
739            .sign_device_keys(&mut device_keys)
740            .await
741            .expect("Should be able to cross-sign a device");
742
743        let device_id = account.device_id().to_owned();
744        let device_data = account.legacy_dehydrate(pickle_key.inner.as_ref());
745        transaction.commit().await.unwrap();
746
747        assign!(put_dehydrated_device::unstable::Request::new(device_id, device_data, device_keys.to_raw()), {
748            one_time_keys, fallback_keys
749        })
750    }
751}