matrix_sdk_crypto/olm/group_sessions/
inbound.rs

1// Copyright 2020 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
15use std::{
16    cmp::Ordering,
17    fmt,
18    ops::Deref,
19    sync::{
20        atomic::{AtomicBool, Ordering::SeqCst},
21        Arc,
22    },
23};
24
25use ruma::{
26    events::room::history_visibility::HistoryVisibility, serde::JsonObject, DeviceKeyAlgorithm,
27    OwnedRoomId, RoomId,
28};
29use serde::{Deserialize, Serialize};
30use tokio::sync::Mutex;
31use vodozemac::{
32    megolm::{
33        DecryptedMessage, DecryptionError, InboundGroupSession as InnerSession,
34        InboundGroupSessionPickle, MegolmMessage, SessionConfig, SessionOrdering,
35    },
36    Curve25519PublicKey, Ed25519PublicKey, PickleError,
37};
38
39use super::{
40    BackedUpRoomKey, ExportedRoomKey, OutboundGroupSession, SenderData, SenderDataType,
41    SessionCreationError, SessionKey,
42};
43use crate::{
44    error::{EventError, MegolmResult},
45    types::{
46        deserialize_curve_key,
47        events::{
48            forwarded_room_key::{
49                ForwardedMegolmV1AesSha2Content, ForwardedMegolmV2AesSha2Content,
50                ForwardedRoomKeyContent,
51            },
52            olm_v1::DecryptedForwardedRoomKeyEvent,
53            room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
54        },
55        serialize_curve_key, EventEncryptionAlgorithm, SigningKeys,
56    },
57};
58
59// TODO add creation times to the inbound group sessions so we can export
60// sessions that were created between some time period, this should only be set
61// for non-imported sessions.
62
63/// Information about the creator of an inbound group session.
64#[derive(Clone)]
65pub(crate) struct SessionCreatorInfo {
66    /// The Curve25519 identity key of the session creator.
67    ///
68    /// If the session was received directly from its creator device through an
69    /// `m.room_key` event (and therefore, session sender == session creator),
70    /// this key equals the Curve25519 device identity key of that device. Since
71    /// this key is one of three keys used to establish the Olm session through
72    /// which encrypted to-device messages (including `m.room_key`) are sent,
73    /// this constitutes a proof that this inbound group session is owned by
74    /// that particular Curve25519 key.
75    ///
76    /// However, if the session was simply forwarded to us in an
77    /// `m.forwarded_room_key` event (in which case sender != creator), this key
78    /// is just a *claim* made by the session sender of what the actual creator
79    /// device is.
80    pub curve25519_key: Curve25519PublicKey,
81
82    /// A mapping of DeviceKeyAlgorithm to the public signing keys of the
83    /// [`Device`] that sent us the session.
84    ///
85    /// If the session was received directly from the creator via an
86    /// `m.room_key` event, this map is taken from the plaintext value of
87    /// the decrypted Olm event, and is a copy of the
88    /// [`DecryptedOlmV1Event::keys`] field as defined in the [spec].
89    ///
90    /// If the session was forwarded to us using an `m.forwarded_room_key`, this
91    /// map is a copy of the claimed Ed25519 key from the content of the
92    /// event.
93    ///
94    /// [spec]: https://spec.matrix.org/unstable/client-server-api/#molmv1curve25519-aes-sha2
95    pub signing_keys: Arc<SigningKeys<DeviceKeyAlgorithm>>,
96}
97
98/// A structure representing an inbound group session.
99///
100/// Inbound group sessions, also known as "room keys", are used to facilitate
101/// the exchange of room messages among a group of participants. The inbound
102/// variant of the group session is used to decrypt the room messages.
103///
104/// This struct wraps the [vodozemac] type of the same name, and adds additional
105/// Matrix-specific data to it. Additionally, the wrapper ensures thread-safe
106/// access of the vodozemac type.
107///
108/// [vodozemac]: https://matrix-org.github.io/vodozemac/vodozemac/index.html
109#[derive(Clone)]
110pub struct InboundGroupSession {
111    inner: Arc<Mutex<InnerSession>>,
112
113    /// A copy of [`InnerSession::session_id`] to avoid having to acquire a lock
114    /// to get to the session ID.
115    session_id: Arc<str>,
116
117    /// A copy of [`InnerSession::first_known_index`] to avoid having to acquire
118    /// a lock to get to the first known index.
119    first_known_index: u32,
120
121    /// Information about the creator of the [`InboundGroupSession`] ("room
122    /// key"). The trustworthiness of the information in this field depends
123    /// on how the session was received.
124    pub(crate) creator_info: SessionCreatorInfo,
125
126    /// Information about the sender of this session and how much we trust that
127    /// information. Holds the information we have about the device that created
128    /// the session, or, if we can use that device information to find the
129    /// sender's cross-signing identity, holds the user ID and cross-signing
130    /// key.
131    pub sender_data: SenderData,
132
133    /// The Room this GroupSession belongs to
134    pub room_id: OwnedRoomId,
135
136    /// A flag recording whether the `InboundGroupSession` was received directly
137    /// as a `m.room_key` event or indirectly via a forward or file import.
138    ///
139    /// If the session is considered to be imported, the information contained
140    /// in the `InboundGroupSession::creator_info` field is not proven to be
141    /// correct.
142    imported: bool,
143
144    /// The messaging algorithm of this [`InboundGroupSession`] as defined by
145    /// the [spec]. Will be one of the `m.megolm.*` algorithms.
146    ///
147    /// [spec]: https://spec.matrix.org/unstable/client-server-api/#messaging-algorithms
148    algorithm: Arc<EventEncryptionAlgorithm>,
149
150    /// The history visibility of the room at the time when the room key was
151    /// created.
152    history_visibility: Arc<Option<HistoryVisibility>>,
153
154    /// Was this room key backed up to the server.
155    backed_up: Arc<AtomicBool>,
156}
157
158impl InboundGroupSession {
159    /// Create a new inbound group session for the given room.
160    ///
161    /// These sessions are used to decrypt room messages.
162    ///
163    /// # Arguments
164    ///
165    /// * `sender_key` - The public Curve25519 key of the account that sent us
166    ///   the session.
167    ///
168    /// * `signing_key` - The public Ed25519 key of the account that sent us the
169    ///   session.
170    ///
171    /// * `room_id` - The id of the room that the session is used in.
172    ///
173    /// * `session_key` - The private session key that is used to decrypt
174    ///   messages.
175    ///
176    /// * `sender_data` - Information about the sender of the to-device message
177    ///   that established this session.
178    pub fn new(
179        sender_key: Curve25519PublicKey,
180        signing_key: Ed25519PublicKey,
181        room_id: &RoomId,
182        session_key: &SessionKey,
183        sender_data: SenderData,
184        encryption_algorithm: EventEncryptionAlgorithm,
185        history_visibility: Option<HistoryVisibility>,
186    ) -> Result<Self, SessionCreationError> {
187        let config = OutboundGroupSession::session_config(&encryption_algorithm)?;
188
189        let session = InnerSession::new(session_key, config);
190        let session_id = session.session_id();
191        let first_known_index = session.first_known_index();
192
193        let mut keys = SigningKeys::new();
194        keys.insert(DeviceKeyAlgorithm::Ed25519, signing_key.into());
195
196        Ok(InboundGroupSession {
197            inner: Arc::new(Mutex::new(session)),
198            history_visibility: history_visibility.into(),
199            session_id: session_id.into(),
200            first_known_index,
201            creator_info: SessionCreatorInfo {
202                curve25519_key: sender_key,
203                signing_keys: keys.into(),
204            },
205            sender_data,
206            room_id: room_id.into(),
207            imported: false,
208            algorithm: encryption_algorithm.into(),
209            backed_up: AtomicBool::new(false).into(),
210        })
211    }
212
213    /// Create a InboundGroupSession from an exported version of the group
214    /// session.
215    ///
216    /// Most notably this can be called with an `ExportedRoomKey` from a
217    /// previous [`export()`] call.
218    ///
219    /// [`export()`]: #method.export
220    pub fn from_export(exported_session: &ExportedRoomKey) -> Result<Self, SessionCreationError> {
221        Self::try_from(exported_session)
222    }
223
224    /// Store the group session as a base64 encoded string.
225    ///
226    /// # Arguments
227    ///
228    /// * `pickle_mode` - The mode that was used to pickle the group session,
229    ///   either an unencrypted mode or an encrypted using passphrase.
230    pub async fn pickle(&self) -> PickledInboundGroupSession {
231        let pickle = self.inner.lock().await.pickle();
232
233        PickledInboundGroupSession {
234            pickle,
235            sender_key: self.creator_info.curve25519_key,
236            signing_key: (*self.creator_info.signing_keys).clone(),
237            sender_data: self.sender_data.clone(),
238            room_id: self.room_id().to_owned(),
239            imported: self.imported,
240            backed_up: self.backed_up(),
241            history_visibility: self.history_visibility.as_ref().clone(),
242            algorithm: (*self.algorithm).to_owned(),
243        }
244    }
245
246    /// Export this session at the first known message index.
247    ///
248    /// If only a limited part of this session should be exported use
249    /// [`export_at_index()`](#method.export_at_index).
250    pub async fn export(&self) -> ExportedRoomKey {
251        self.export_at_index(self.first_known_index()).await
252    }
253
254    /// Get the sender key that this session was received from.
255    pub fn sender_key(&self) -> Curve25519PublicKey {
256        self.creator_info.curve25519_key
257    }
258
259    /// Has the session been backed up to the server.
260    pub fn backed_up(&self) -> bool {
261        self.backed_up.load(SeqCst)
262    }
263
264    /// Reset the backup state of the inbound group session.
265    pub fn reset_backup_state(&self) {
266        self.backed_up.store(false, SeqCst)
267    }
268
269    /// For testing, allow to manually mark this GroupSession to have been
270    /// backed up
271    pub fn mark_as_backed_up(&self) {
272        self.backed_up.store(true, SeqCst)
273    }
274
275    /// Get the map of signing keys this session was received from.
276    pub fn signing_keys(&self) -> &SigningKeys<DeviceKeyAlgorithm> {
277        &self.creator_info.signing_keys
278    }
279
280    /// Export this session at the given message index.
281    pub async fn export_at_index(&self, message_index: u32) -> ExportedRoomKey {
282        let message_index = std::cmp::max(self.first_known_index(), message_index);
283
284        let session_key =
285            self.inner.lock().await.export_at(message_index).expect("Can't export session");
286
287        ExportedRoomKey {
288            algorithm: self.algorithm().to_owned(),
289            room_id: self.room_id().to_owned(),
290            sender_key: self.creator_info.curve25519_key,
291            session_id: self.session_id().to_owned(),
292            forwarding_curve25519_key_chain: vec![],
293            sender_claimed_keys: (*self.creator_info.signing_keys).clone(),
294            session_key,
295        }
296    }
297
298    /// Restore a Session from a previously pickled string.
299    ///
300    /// Returns the restored group session or a `UnpicklingError` if there
301    /// was an error.
302    ///
303    /// # Arguments
304    ///
305    /// * `pickle` - The pickled version of the `InboundGroupSession`.
306    ///
307    /// * `pickle_mode` - The mode that was used to pickle the session, either
308    ///   an unencrypted mode or an encrypted using passphrase.
309    pub fn from_pickle(pickle: PickledInboundGroupSession) -> Result<Self, PickleError> {
310        let session: InnerSession = pickle.pickle.into();
311        let first_known_index = session.first_known_index();
312        let session_id = session.session_id();
313
314        Ok(InboundGroupSession {
315            inner: Mutex::new(session).into(),
316            session_id: session_id.into(),
317            creator_info: SessionCreatorInfo {
318                curve25519_key: pickle.sender_key,
319                signing_keys: pickle.signing_key.into(),
320            },
321            sender_data: pickle.sender_data,
322            history_visibility: pickle.history_visibility.into(),
323            first_known_index,
324            room_id: (*pickle.room_id).into(),
325            backed_up: AtomicBool::from(pickle.backed_up).into(),
326            algorithm: pickle.algorithm.into(),
327            imported: pickle.imported,
328        })
329    }
330
331    /// The room where this session is used in.
332    pub fn room_id(&self) -> &RoomId {
333        &self.room_id
334    }
335
336    /// Returns the unique identifier for this session.
337    pub fn session_id(&self) -> &str {
338        &self.session_id
339    }
340
341    /// The algorithm that this inbound group session is using to decrypt
342    /// events.
343    pub fn algorithm(&self) -> &EventEncryptionAlgorithm {
344        &self.algorithm
345    }
346
347    /// Get the first message index we know how to decrypt.
348    pub fn first_known_index(&self) -> u32 {
349        self.first_known_index
350    }
351
352    /// Has the session been imported from a file or server-side backup? As
353    /// opposed to being directly received as an `m.room_key` event.
354    pub fn has_been_imported(&self) -> bool {
355        self.imported
356    }
357
358    /// Check if the [`InboundGroupSession`] is better than the given other
359    /// [`InboundGroupSession`]
360    pub async fn compare(&self, other: &InboundGroupSession) -> SessionOrdering {
361        // If this is the same object the ordering is the same, we can't compare because
362        // we would deadlock while trying to acquire the same lock twice.
363        if Arc::ptr_eq(&self.inner, &other.inner) {
364            SessionOrdering::Equal
365        } else if self.sender_key() != other.sender_key()
366            || self.signing_keys() != other.signing_keys()
367            || self.algorithm() != other.algorithm()
368            || self.room_id() != other.room_id()
369        {
370            SessionOrdering::Unconnected
371        } else {
372            let mut other_inner = other.inner.lock().await;
373
374            match self.inner.lock().await.compare(&mut other_inner) {
375                SessionOrdering::Equal => {
376                    match self.sender_data.compare_trust_level(&other.sender_data) {
377                        Ordering::Less => SessionOrdering::Worse,
378                        Ordering::Equal => SessionOrdering::Equal,
379                        Ordering::Greater => SessionOrdering::Better,
380                    }
381                }
382                result => result,
383            }
384        }
385    }
386
387    /// Decrypt the given ciphertext.
388    ///
389    /// Returns the decrypted plaintext or an `DecryptionError` if
390    /// decryption failed.
391    ///
392    /// # Arguments
393    ///
394    /// * `message` - The message that should be decrypted.
395    pub(crate) async fn decrypt_helper(
396        &self,
397        message: &MegolmMessage,
398    ) -> Result<DecryptedMessage, DecryptionError> {
399        self.inner.lock().await.decrypt(message)
400    }
401
402    /// Export the inbound group session into a format that can be uploaded to
403    /// the server as a backup.
404    pub async fn to_backup(&self) -> BackedUpRoomKey {
405        self.export().await.into()
406    }
407
408    /// Decrypt an event from a room timeline.
409    ///
410    /// # Arguments
411    ///
412    /// * `event` - The event that should be decrypted.
413    pub async fn decrypt(&self, event: &EncryptedEvent) -> MegolmResult<(JsonObject, u32)> {
414        let decrypted = match &event.content.scheme {
415            RoomEventEncryptionScheme::MegolmV1AesSha2(c) => {
416                self.decrypt_helper(&c.ciphertext).await?
417            }
418            #[cfg(feature = "experimental-algorithms")]
419            RoomEventEncryptionScheme::MegolmV2AesSha2(c) => {
420                self.decrypt_helper(&c.ciphertext).await?
421            }
422            RoomEventEncryptionScheme::Unknown(_) => {
423                return Err(EventError::UnsupportedAlgorithm.into());
424            }
425        };
426
427        let plaintext = String::from_utf8_lossy(&decrypted.plaintext);
428
429        let mut decrypted_object = serde_json::from_str::<JsonObject>(&plaintext)?;
430
431        let server_ts: i64 = event.origin_server_ts.0.into();
432
433        decrypted_object.insert("sender".to_owned(), event.sender.to_string().into());
434        decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into());
435        decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into());
436
437        let room_id = decrypted_object
438            .get("room_id")
439            .and_then(|r| r.as_str().and_then(|r| RoomId::parse(r).ok()));
440
441        // Check that we have a room id and that the event wasn't forwarded from
442        // another room.
443        if room_id.as_deref() != Some(self.room_id()) {
444            return Err(EventError::MismatchedRoom(self.room_id().to_owned(), room_id).into());
445        }
446
447        decrypted_object.insert(
448            "unsigned".to_owned(),
449            serde_json::to_value(&event.unsigned).unwrap_or_default(),
450        );
451
452        if let Some(decrypted_content) =
453            decrypted_object.get_mut("content").and_then(|c| c.as_object_mut())
454        {
455            if !decrypted_content.contains_key("m.relates_to") {
456                if let Some(relation) = &event.content.relates_to {
457                    decrypted_content.insert("m.relates_to".to_owned(), relation.to_owned());
458                }
459            }
460        }
461
462        Ok((decrypted_object, decrypted.message_index))
463    }
464
465    /// For test only, mark this session as imported.
466    #[cfg(test)]
467    pub(crate) fn mark_as_imported(&mut self) {
468        self.imported = true;
469    }
470
471    /// Return the [`SenderDataType`] of our [`SenderData`]. This is used during
472    /// serialization, to allow us to store the type in a separate queryable
473    /// column/property.
474    pub fn sender_data_type(&self) -> SenderDataType {
475        self.sender_data.to_type()
476    }
477}
478
479#[cfg(not(tarpaulin_include))]
480impl fmt::Debug for InboundGroupSession {
481    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482        f.debug_struct("InboundGroupSession").field("session_id", &self.session_id()).finish()
483    }
484}
485
486impl PartialEq for InboundGroupSession {
487    fn eq(&self, other: &Self) -> bool {
488        self.session_id() == other.session_id()
489    }
490}
491
492/// A pickled version of an `InboundGroupSession`.
493///
494/// Holds all the information that needs to be stored in a database to restore
495/// an InboundGroupSession.
496#[derive(Serialize, Deserialize)]
497#[allow(missing_debug_implementations)]
498pub struct PickledInboundGroupSession {
499    /// The pickle string holding the InboundGroupSession.
500    pub pickle: InboundGroupSessionPickle,
501    /// The public Curve25519 key of the account that sent us the session
502    #[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
503    pub sender_key: Curve25519PublicKey,
504    /// The public ed25519 key of the account that sent us the session.
505    pub signing_key: SigningKeys<DeviceKeyAlgorithm>,
506    /// Information on the device/sender who sent us this session
507    #[serde(default)]
508    pub sender_data: SenderData,
509    /// The id of the room that the session is used in.
510    pub room_id: OwnedRoomId,
511    /// Flag remembering if the session was directly sent to us by the sender
512    /// or if it was imported.
513    pub imported: bool,
514    /// Flag remembering if the session has been backed up.
515    #[serde(default)]
516    pub backed_up: bool,
517    /// History visibility of the room when the session was created.
518    pub history_visibility: Option<HistoryVisibility>,
519    /// The algorithm of this inbound group session.
520    #[serde(default = "default_algorithm")]
521    pub algorithm: EventEncryptionAlgorithm,
522}
523
524fn default_algorithm() -> EventEncryptionAlgorithm {
525    EventEncryptionAlgorithm::MegolmV1AesSha2
526}
527
528impl TryFrom<&ExportedRoomKey> for InboundGroupSession {
529    type Error = SessionCreationError;
530
531    fn try_from(key: &ExportedRoomKey) -> Result<Self, Self::Error> {
532        let config = OutboundGroupSession::session_config(&key.algorithm)?;
533        let session = InnerSession::import(&key.session_key, config);
534        let first_known_index = session.first_known_index();
535
536        Ok(InboundGroupSession {
537            inner: Mutex::new(session).into(),
538            session_id: key.session_id.to_owned().into(),
539            creator_info: SessionCreatorInfo {
540                curve25519_key: key.sender_key,
541                signing_keys: key.sender_claimed_keys.to_owned().into(),
542            },
543            // TODO: In future, exported keys should contain sender data that we can use here.
544            // See https://github.com/matrix-org/matrix-rust-sdk/issues/3548
545            sender_data: SenderData::default(),
546            history_visibility: None.into(),
547            first_known_index,
548            room_id: key.room_id.to_owned(),
549            imported: true,
550            algorithm: key.algorithm.to_owned().into(),
551            backed_up: AtomicBool::from(false).into(),
552        })
553    }
554}
555
556impl From<&ForwardedMegolmV1AesSha2Content> for InboundGroupSession {
557    fn from(value: &ForwardedMegolmV1AesSha2Content) -> Self {
558        let session = InnerSession::import(&value.session_key, SessionConfig::version_1());
559        let session_id = session.session_id().into();
560        let first_known_index = session.first_known_index();
561
562        InboundGroupSession {
563            inner: Mutex::new(session).into(),
564            session_id,
565            creator_info: SessionCreatorInfo {
566                curve25519_key: value.claimed_sender_key,
567                signing_keys: SigningKeys::from([(
568                    DeviceKeyAlgorithm::Ed25519,
569                    value.claimed_ed25519_key.into(),
570                )])
571                .into(),
572            },
573            // In future, exported keys should contain sender data that we can use here.
574            // See https://github.com/matrix-org/matrix-rust-sdk/issues/3548
575            sender_data: SenderData::default(),
576            history_visibility: None.into(),
577            first_known_index,
578            room_id: value.room_id.to_owned(),
579            imported: true,
580            algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(),
581            backed_up: AtomicBool::from(false).into(),
582        }
583    }
584}
585
586impl From<&ForwardedMegolmV2AesSha2Content> for InboundGroupSession {
587    fn from(value: &ForwardedMegolmV2AesSha2Content) -> Self {
588        let session = InnerSession::import(&value.session_key, SessionConfig::version_2());
589        let session_id = session.session_id().into();
590        let first_known_index = session.first_known_index();
591
592        InboundGroupSession {
593            inner: Mutex::new(session).into(),
594            session_id,
595            creator_info: SessionCreatorInfo {
596                curve25519_key: value.claimed_sender_key,
597                signing_keys: value.claimed_signing_keys.to_owned().into(),
598            },
599            // In future, exported keys should contain sender data that we can use here.
600            // See https://github.com/matrix-org/matrix-rust-sdk/issues/3548
601            sender_data: SenderData::default(),
602            history_visibility: None.into(),
603            first_known_index,
604            room_id: value.room_id.to_owned(),
605            imported: true,
606            algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(),
607            backed_up: AtomicBool::from(false).into(),
608        }
609    }
610}
611
612impl TryFrom<&DecryptedForwardedRoomKeyEvent> for InboundGroupSession {
613    type Error = SessionCreationError;
614
615    fn try_from(value: &DecryptedForwardedRoomKeyEvent) -> Result<Self, Self::Error> {
616        match &value.content {
617            ForwardedRoomKeyContent::MegolmV1AesSha2(c) => Ok(Self::from(c.deref())),
618            #[cfg(feature = "experimental-algorithms")]
619            ForwardedRoomKeyContent::MegolmV2AesSha2(c) => Ok(Self::from(c.deref())),
620            ForwardedRoomKeyContent::Unknown(c) => {
621                Err(SessionCreationError::Algorithm(c.algorithm.to_owned()))
622            }
623        }
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use assert_matches2::assert_let;
630    use matrix_sdk_test::async_test;
631    use ruma::{
632        device_id, events::room::history_visibility::HistoryVisibility, room_id, user_id, DeviceId,
633        UserId,
634    };
635    use vodozemac::{
636        megolm::{SessionKey, SessionOrdering},
637        Curve25519PublicKey, Ed25519PublicKey,
638    };
639
640    use crate::{
641        olm::{InboundGroupSession, KnownSenderData, SenderData},
642        types::EventEncryptionAlgorithm,
643        Account,
644    };
645
646    fn alice_id() -> &'static UserId {
647        user_id!("@alice:example.org")
648    }
649
650    fn alice_device_id() -> &'static DeviceId {
651        device_id!("ALICEDEVICE")
652    }
653
654    #[async_test]
655    async fn test_can_deserialise_pickled_session_without_sender_data() {
656        // Given the raw JSON for a picked inbound group session without any sender_data
657        let pickle = r#"
658        {
659            "pickle": {
660                "initial_ratchet": {
661                    "inner": [ 124, 251, 213, 204, 108, 247, 54, 7, 179, 162, 15, 107, 154, 215,
662                               220, 46, 123, 113, 120, 162, 225, 246, 237, 203, 125, 102, 190, 212,
663                               229, 195, 136, 185, 26, 31, 77, 140, 144, 181, 152, 177, 46, 105,
664                               202, 6, 53, 158, 157, 170, 31, 155, 130, 87, 214, 110, 143, 55, 68,
665                               138, 41, 35, 242, 230, 194, 15, 16, 145, 116, 94, 89, 35, 79, 145,
666                               245, 117, 204, 173, 166, 178, 49, 131, 143, 61, 61, 15, 211, 167, 17,
667                               2, 79, 110, 149, 200, 223, 23, 185, 200, 29, 64, 55, 39, 147, 167,
668                               205, 224, 159, 101, 218, 249, 203, 30, 175, 174, 48, 252, 40, 131,
669                               52, 135, 91, 57, 211, 96, 105, 58, 55, 68, 250, 24 ],
670                    "counter": 0
671                },
672                "signing_key": [ 93, 185, 171, 61, 173, 100, 51, 9, 157, 180, 214, 39, 131, 80, 118,
673                                 130, 199, 232, 163, 197, 45, 23, 227, 100, 151, 59, 19, 102, 38,
674                                 149, 43, 38 ],
675                "signing_key_verified": true,
676                "config": {
677                  "version": "V1"
678                }
679            },
680            "sender_key": "AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8",
681            "signing_key": {
682                "ed25519": "wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"
683            },
684            "room_id": "!test:localhost",
685            "forwarding_chains": ["tb6kQKjk+SJl2KnfQ0lKVOZl6gDFMcsb9HcUP9k/4hc"],
686            "imported": false,
687            "backed_up": false,
688            "history_visibility": "shared",
689            "algorithm": "m.megolm.v1.aes-sha2"
690        }
691        "#;
692
693        // When we deserialise it to from JSON
694        let deserialized = serde_json::from_str(pickle).unwrap();
695
696        // And unpickle it
697        let unpickled = InboundGroupSession::from_pickle(deserialized).unwrap();
698
699        // Then it was parsed correctly
700        assert_eq!(unpickled.session_id(), "XbmrPa1kMwmdtNYng1B2gsfoo8UtF+NklzsTZiaVKyY");
701
702        // And we populated the InboundGroupSession's sender_data with a default value,
703        // with legacy_session set to true.
704        assert_let!(
705            SenderData::UnknownDevice { legacy_session, owner_check_failed } =
706                unpickled.sender_data
707        );
708        assert!(legacy_session);
709        assert!(!owner_check_failed);
710    }
711
712    #[async_test]
713    async fn test_can_serialise_pickled_session_with_sender_data() {
714        // Given an InboundGroupSession
715        let igs = InboundGroupSession::new(
716            Curve25519PublicKey::from_base64("AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8")
717                .unwrap(),
718            Ed25519PublicKey::from_base64("wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww").unwrap(),
719            room_id!("!test:localhost"),
720            &create_session_key(),
721            SenderData::unknown(),
722            EventEncryptionAlgorithm::MegolmV1AesSha2,
723            Some(HistoryVisibility::Shared),
724        )
725        .unwrap();
726
727        // When we pickle it
728        let pickled = igs.pickle().await;
729
730        // And serialise it
731        let serialised = serde_json::to_string(&pickled).unwrap();
732
733        // Then it looks as we expect
734
735        // (Break out this list of numbers as otherwise it bothers the json macro below)
736        let expected_inner = vec![
737            193, 203, 223, 152, 33, 132, 200, 168, 24, 197, 79, 174, 231, 202, 45, 245, 128, 131,
738            178, 165, 148, 37, 241, 214, 178, 218, 25, 33, 68, 48, 153, 104, 122, 6, 249, 198, 97,
739            226, 214, 75, 64, 128, 25, 138, 98, 90, 138, 93, 52, 206, 174, 3, 84, 149, 101, 140,
740            238, 156, 103, 107, 124, 144, 139, 104, 253, 5, 100, 251, 186, 118, 208, 87, 31, 218,
741            123, 234, 103, 34, 246, 100, 39, 90, 216, 72, 187, 86, 202, 150, 100, 116, 204, 254,
742            10, 154, 216, 133, 61, 250, 75, 100, 195, 63, 138, 22, 17, 13, 156, 123, 195, 132, 111,
743            95, 250, 24, 236, 0, 246, 93, 230, 100, 211, 165, 211, 190, 181, 87, 42, 181,
744        ];
745        assert_eq!(
746            serde_json::from_str::<serde_json::Value>(&serialised).unwrap(),
747            serde_json::json!({
748                "pickle":{
749                    "initial_ratchet":{
750                        "inner": expected_inner,
751                        "counter":0
752                    },
753                    "signing_key":[
754                        213,161,95,135,114,153,162,127,217,74,64,2,59,143,93,5,190,157,120,
755                        80,89,8,87,129,115,148,104,144,152,186,178,109
756                    ],
757                    "signing_key_verified":true,
758                    "config":{"version":"V1"}
759                },
760                "sender_key":"AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8",
761                "signing_key":{"ed25519":"wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"},
762                "sender_data":{
763                    "UnknownDevice":{
764                        "legacy_session":false
765                    }
766                },
767                "room_id":"!test:localhost",
768                "imported":false,
769                "backed_up":false,
770                "history_visibility":"shared",
771                "algorithm":"m.megolm.v1.aes-sha2"
772            })
773        );
774    }
775
776    #[async_test]
777    async fn test_can_deserialise_pickled_session_with_sender_data() {
778        // Given the raw JSON for a picked inbound group session (including sender_data)
779        let pickle = r#"
780        {
781            "pickle": {
782                "initial_ratchet": {
783                    "inner": [ 124, 251, 213, 204, 108, 247, 54, 7, 179, 162, 15, 107, 154, 215,
784                               220, 46, 123, 113, 120, 162, 225, 246, 237, 203, 125, 102, 190, 212,
785                               229, 195, 136, 185, 26, 31, 77, 140, 144, 181, 152, 177, 46, 105,
786                               202, 6, 53, 158, 157, 170, 31, 155, 130, 87, 214, 110, 143, 55, 68,
787                               138, 41, 35, 242, 230, 194, 15, 16, 145, 116, 94, 89, 35, 79, 145,
788                               245, 117, 204, 173, 166, 178, 49, 131, 143, 61, 61, 15, 211, 167, 17,
789                               2, 79, 110, 149, 200, 223, 23, 185, 200, 29, 64, 55, 39, 147, 167,
790                               205, 224, 159, 101, 218, 249, 203, 30, 175, 174, 48, 252, 40, 131,
791                               52, 135, 91, 57, 211, 96, 105, 58, 55, 68, 250, 24 ],
792                    "counter": 0
793                },
794                "signing_key": [ 93, 185, 171, 61, 173, 100, 51, 9, 157, 180, 214, 39, 131, 80, 118,
795                                 130, 199, 232, 163, 197, 45, 23, 227, 100, 151, 59, 19, 102, 38,
796                                 149, 43, 38 ],
797                "signing_key_verified": true,
798                "config": {
799                  "version": "V1"
800                }
801            },
802            "sender_key": "AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8",
803            "signing_key": {
804                "ed25519": "wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"
805            },
806            "sender_data":{
807                "UnknownDevice":{
808                    "legacy_session":false
809                }
810            },
811            "room_id": "!test:localhost",
812            "forwarding_chains": ["tb6kQKjk+SJl2KnfQ0lKVOZl6gDFMcsb9HcUP9k/4hc"],
813            "imported": false,
814            "backed_up": false,
815            "history_visibility": "shared",
816            "algorithm": "m.megolm.v1.aes-sha2"
817        }
818        "#;
819
820        // When we deserialise it to from JSON
821        let deserialized = serde_json::from_str(pickle).unwrap();
822
823        // And unpickle it
824        let unpickled = InboundGroupSession::from_pickle(deserialized).unwrap();
825
826        // Then it was parsed correctly
827        assert_eq!(unpickled.session_id(), "XbmrPa1kMwmdtNYng1B2gsfoo8UtF+NklzsTZiaVKyY");
828
829        // And we populated the InboundGroupSession's sender_data with the provided
830        // values
831        assert_let!(
832            SenderData::UnknownDevice { legacy_session, owner_check_failed } =
833                unpickled.sender_data
834        );
835        assert!(!legacy_session);
836        assert!(!owner_check_failed);
837    }
838
839    #[async_test]
840    async fn test_session_comparison() {
841        let alice = Account::with_device_id(alice_id(), alice_device_id());
842        let room_id = room_id!("!test:localhost");
843
844        let (_, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
845
846        let worse = InboundGroupSession::from_export(&inbound.export_at_index(10).await).unwrap();
847        let mut copy = InboundGroupSession::from_pickle(inbound.pickle().await).unwrap();
848
849        assert_eq!(inbound.compare(&worse).await, SessionOrdering::Better);
850        assert_eq!(worse.compare(&inbound).await, SessionOrdering::Worse);
851        assert_eq!(inbound.compare(&inbound).await, SessionOrdering::Equal);
852        assert_eq!(inbound.compare(&copy).await, SessionOrdering::Equal);
853
854        copy.creator_info.curve25519_key =
855            Curve25519PublicKey::from_base64("XbmrPa1kMwmdtNYng1B2gsfoo8UtF+NklzsTZiaVKyY")
856                .unwrap();
857
858        assert_eq!(inbound.compare(&copy).await, SessionOrdering::Unconnected);
859    }
860
861    #[async_test]
862    async fn test_session_comparison_sender_data() {
863        let alice = Account::with_device_id(alice_id(), alice_device_id());
864        let room_id = room_id!("!test:localhost");
865
866        let (_, mut inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
867
868        let sender_data = SenderData::SenderVerified(KnownSenderData {
869            user_id: alice.user_id().into(),
870            device_id: Some(alice.device_id().into()),
871            master_key: alice.identity_keys().ed25519.into(),
872        });
873
874        let mut better = InboundGroupSession::from_pickle(inbound.pickle().await).unwrap();
875        better.sender_data = sender_data.clone();
876
877        assert_eq!(inbound.compare(&better).await, SessionOrdering::Worse);
878        assert_eq!(better.compare(&inbound).await, SessionOrdering::Better);
879
880        inbound.sender_data = sender_data;
881        assert_eq!(better.compare(&inbound).await, SessionOrdering::Equal);
882    }
883
884    fn create_session_key() -> SessionKey {
885        SessionKey::from_base64(
886            "\
887            AgAAAADBy9+YIYTIqBjFT67nyi31gIOypZQl8day2hkhRDCZaHoG+cZh4tZLQIAZimJail0\
888            0zq4DVJVljO6cZ2t8kIto/QVk+7p20Fcf2nvqZyL2ZCda2Ei7VsqWZHTM/gqa2IU9+ktkwz\
889            +KFhENnHvDhG9f+hjsAPZd5mTTpdO+tVcqtdWhX4dymaJ/2UpAAjuPXQW+nXhQWQhXgXOUa\
890            JCYurJtvbCbqZGeDMmVIoqukBs2KugNJ6j5WlTPoeFnMl6Guy9uH2iWWxGg8ZgT2xspqVl5\
891            CwujjC+m7Dh1toVkvu+bAw\
892            ",
893        )
894        .unwrap()
895    }
896}