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    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/// Error type for device dehydration issues.
64#[derive(Debug, Error)]
65pub enum DehydrationError {
66    /// The legacy dehydrated device could not be unpickled.
67    #[error(transparent)]
68    LegacyPickle(#[from] LibolmPickleError),
69
70    /// The dehydrated device could not be unpickled.
71    #[error(transparent)]
72    Pickle(#[from] DehydratedDeviceError),
73
74    /// The pickle key has an invalid length
75    #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
76    PickleKeyLength(usize),
77
78    /// The dehydrated device could not be signed by our user identity,
79    /// we're missing the self-signing key.
80    #[error("The self-signing key is missing, can't create a dehydrated device")]
81    MissingSigningKey(#[from] SignatureError),
82
83    /// We could not deserialize the dehydrated device data.
84    #[error(transparent)]
85    Json(#[from] serde_json::Error),
86
87    /// The store ran into an error.
88    #[error(transparent)]
89    Store(#[from] CryptoStoreError),
90}
91
92/// Struct collecting methods to create and rehydrate dehydrated devices.
93#[derive(Debug)]
94pub struct DehydratedDevices {
95    pub(crate) inner: OlmMachine,
96}
97
98impl DehydratedDevices {
99    /// Create a new [`DehydratedDevice`] which can be uploaded to the server.
100    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    /// Rehydrate the dehydrated device.
122    ///
123    /// Once rehydrated, to-device events can be pushed into the
124    /// [`RehydratedDevice`] to collect the room keys the device has
125    /// received.
126    ///
127    /// For more info see the example for the
128    /// [`RehydratedDevice::receive_events()`] method.
129    ///
130    /// # Arguments
131    ///
132    /// * `pickle_key` - The encryption key that was used to encrypt the private
133    ///   parts of the identity keys, and one-time keys of the device.
134    ///
135    /// * `device_id` - The unique identifier of the device.
136    ///
137    /// * `device_data` - The encrypted data of the device, containing the
138    ///   private keys of the device.
139    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    /// Get the cached dehydrated device pickle key if any.
152    ///
153    /// None if the key was not previously cached (via
154    /// [`DehydratedDevices::save_dehydrated_device_pickle_key`]).
155    ///
156    /// Should be used to periodically rotate the dehydrated device to avoid
157    /// one-time keys exhaustion and accumulation of to_device messages.
158    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    /// Store the dehydrated device pickle key in the crypto store.
165    ///
166    /// This is useful if the client wants to periodically rotate dehydrated
167    /// devices to avoid one-time keys exhaustion and accumulated to_device
168    /// problems.
169    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    /// Deletes the previously stored dehydrated device pickle key.
181    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/// A rehydraded device.
187///
188/// This device can now receive to-device events to decrypt and gather room keys
189/// which were sent to the dehydrated device.
190#[derive(Debug)]
191pub struct RehydratedDevice {
192    rehydrated: OlmMachine,
193    original: OlmMachine,
194}
195
196impl RehydratedDevice {
197    /// Feed to-device events the device was supposed to receive into the
198    /// [`RehydratedDevice`].
199    ///
200    /// Most to-device events we feed into the [`RehydratedDevice`] will contain
201    /// room keys, the rehydrated device will pass these room keys into our
202    /// own [`OlmMachine`] which will persist them and make the room keys
203    /// available for use using the usual
204    /// [`OlmMachine::decrypt_room_event()`] method.
205    ///
206    /// Once the homeserver returns a response without any to-device events, we
207    /// can safely delete the current dehydrated device and create a new one.
208    ///
209    /// # Examples
210    ///
211    /// ```no_run
212    /// # use anyhow::Result;
213    /// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey };
214    /// # use ruma::{api::client::dehydrated_device, DeviceId};
215    /// # async fn example() -> Result<()> {
216    /// # let machine: OlmMachine = unimplemented!();
217    /// async fn get_dehydrated_device() -> Result<dehydrated_device::get_dehydrated_device::unstable::Response> {
218    ///     todo!("Download the dehydrated device");
219    /// }
220    ///
221    /// async fn get_events(
222    ///     device_id: &DeviceId,
223    ///     since_token: Option<&str>
224    /// ) -> Result<dehydrated_device::get_events::unstable::Response> {
225    ///     todo!("Download the to-device events of the dehydrated device");
226    /// }
227    /// // Get the cached dehydrated key (got it after verification/recovery)
228    /// let pickle_key = machine
229    ///     .dehydrated_devices().get_dehydrated_device_pickle_key().await?.unwrap();
230    ///
231    /// // Fetch the dehydrated device from the server.
232    /// let response = get_dehydrated_device().await?;
233    /// let device_id = response.device_id;
234    ///
235    /// // Rehydrate the device.
236    /// let rehydrated = machine
237    ///     .dehydrated_devices()
238    ///     .rehydrate(&pickle_key, &device_id, response.device_data)
239    ///     .await?;
240    ///
241    /// let mut since_token = None;
242    /// let mut imported_room_keys = 0;
243    ///
244    /// loop {
245    ///     let response =
246    ///         get_events(&device_id, since_token).await?;
247    ///
248    ///     if response.events.is_empty() {
249    ///         break;
250    ///     }
251    ///
252    ///     since_token = response.next_batch.as_deref();
253    ///     imported_room_keys += rehydrated.receive_events(response.events).await?.len();
254    /// }
255    ///
256    /// println!("Successfully imported {imported_room_keys} from the dehydrated device.");
257    /// # Ok(())
258    /// # }
259    /// ```
260    #[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 us first give the events to the rehydrated device, this will decrypt any
283        // encrypted to-device events and fetch out the room keys.
284        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        // Now take the room keys and persist them in our original `OlmMachine`.
292        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/// A dehydrated device that can uploaded to the homeserver.
307///
308/// To upload the dehydrated device take a look at the
309/// [`DehydratedDevice::keys_for_upload()`] method.
310#[derive(Debug)]
311pub struct DehydratedDevice {
312    store: Store,
313}
314
315impl DehydratedDevice {
316    /// Get the request to upload the dehydrated device.
317    ///
318    /// # Arguments
319    ///
320    /// * `initial_device_display_name` - The human-readable name this device
321    ///   should have.
322    /// * `pickle_key` - The encryption key that should be used to encrypt the
323    ///   private parts of the identity keys, and one-time keys of the device.
324    ///
325    /// # Examples
326    ///
327    /// ```no_run
328    /// # use matrix_sdk_crypto::OlmMachine;    /// #
329    /// use matrix_sdk_crypto::store::DehydratedDeviceKey;
330    ///
331    /// async fn example() -> anyhow::Result<()> {
332    /// # let machine: OlmMachine = unimplemented!();
333    /// // Create a new random key
334    /// let pickle_key = DehydratedDeviceKey::new()?;
335    ///
336    /// // Create the dehydrated device.
337    /// let device = machine.dehydrated_devices().create().await?;
338    ///
339    /// // Create the request that should upload the device.
340    /// let request = device
341    ///     .keys_for_upload("Dehydrated device".to_owned(), &pickle_key)
342    ///     .await?;
343    ///
344    /// // Save the key if you want to later one rotate the dehydrated device
345    /// machine.dehydrated_devices().save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
346    ///
347    /// // Send the request out using your HTTP client.
348    /// // client.send(request).await?;
349    /// # Ok(())
350    /// # }
351    /// ```
352    #[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    // Insert some device keys into a [`OlmMachine`] making the [`Device`] available
440    // to the [`OlmMachine`].
441    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        // Ensure that we know about the public keys of the dehydrated device.
535        receive_device_keys(&alice, user_id(), &request.device_id, request.device_keys).await;
536        // Create a 1-to-1 Olm session with the dehydrated device.
537        create_session(&alice, user_id(), &request.device_id, key_id, one_time_key).await;
538
539        // Send a room key to the dehydrated device.
540        let (event, group_session) = send_room_key(&alice, room_id, user_id()).await;
541
542        // Let's now create a new `OlmMachine` which doesn't know about the room key.
543        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        // Rehydrate the device.
557        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        // Push the to-device event containing the room key into the rehydrated device.
567        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        // The `OlmMachine` now does know about the room key since the rehydrated device
575        // shared it with us.
576        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        // Rehydrate the device.
615        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    /// Test that we can rehydrate an older version of dehydrated device
630    #[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        // Ensure that we know about the public keys of the dehydrated device.
647        receive_device_keys(&alice, user_id(), &device_id, request.device_keys).await;
648        // Create a 1-to-1 Olm session with the dehydrated device.
649        create_session(&alice, user_id(), &device_id, key_id, one_time_key).await;
650
651        // Send a room key to the dehydrated device.
652        let (event, group_session) = send_room_key(&alice, room_id, user_id()).await;
653
654        // Let's now create a new `OlmMachine` which doesn't know about the room key.
655        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        // Rehydrate the device.
669        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        // Push the to-device event containing the room key into the rehydrated device.
679        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        // The `OlmMachine` now does know about the room key since the rehydrated device
687        // shared it with us.
688        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    /// Duplicates the behaviour of [`DehydratedDevice::keys_for_upload`],
703    /// except that it calls [`Account::legacy_dehydrate`] instead of
704    /// [`Account::dehydrate`].
705    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}