Skip to main content

matrix_sdk_crypto/olm/
session.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::{fmt, sync::Arc};
16
17use ruma::{SecondsSinceUnixEpoch, serde::Raw};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use tokio::sync::Mutex;
21use tracing::{Span, debug};
22use vodozemac::{
23    Curve25519PublicKey,
24    olm::{DecryptionError, OlmMessage, Session as InnerSession, SessionConfig, SessionPickle},
25};
26
27#[cfg(feature = "experimental-algorithms")]
28use crate::types::events::room::encrypted::OlmV2Curve25519AesSha2Content;
29use crate::{
30    DeviceData,
31    error::{EventError, OlmResult, SessionUnpickleError},
32    types::{
33        DeviceKeys, EventEncryptionAlgorithm,
34        events::{
35            EventType,
36            olm_v1::{DecryptedOlmV1Event, OlmV1Keys},
37            room::encrypted::{OlmV1Curve25519AesSha2Content, ToDeviceEncryptedEventContent},
38        },
39    },
40};
41
42/// Cryptographic session that enables secure communication between two
43/// `Account`s
44#[derive(Clone)]
45pub struct Session {
46    /// The OlmSession
47    pub inner: Arc<Mutex<InnerSession>>,
48    /// Our sessionId
49    pub session_id: Arc<str>,
50    /// The Key of the sender
51    pub sender_key: Curve25519PublicKey,
52    /// Our own signed device keys
53    pub our_device_keys: DeviceKeys,
54    /// Has this been created using the fallback key
55    pub created_using_fallback_key: bool,
56    /// When the session was created
57    pub creation_time: SecondsSinceUnixEpoch,
58    /// When the session was last used
59    pub last_use_time: SecondsSinceUnixEpoch,
60}
61
62#[cfg(not(tarpaulin_include))]
63impl fmt::Debug for Session {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.debug_struct("Session")
66            .field("session_id", &self.session_id())
67            .field("sender_key", &self.sender_key)
68            .finish()
69    }
70}
71
72impl Session {
73    /// Decrypt the given Olm message.
74    ///
75    /// Returns the decrypted plaintext or a [`DecryptionError`] if decryption
76    /// failed.
77    ///
78    /// # Arguments
79    ///
80    /// - `message` - The Olm message that should be decrypted.
81    pub async fn decrypt(&mut self, message: &OlmMessage) -> Result<String, DecryptionError> {
82        let mut inner = self.inner.lock().await;
83        Span::current().record("session_id", inner.session_id());
84
85        let plaintext = inner.decrypt(message)?;
86        debug!(session=?inner, "Decrypted an Olm message");
87
88        let plaintext = String::from_utf8_lossy(&plaintext).to_string();
89
90        self.last_use_time = SecondsSinceUnixEpoch::now();
91
92        Ok(plaintext)
93    }
94
95    /// Get the sender key that was used to establish this Session.
96    pub fn sender_key(&self) -> Curve25519PublicKey {
97        self.sender_key
98    }
99
100    /// Get the [`SessionConfig`] that this session is using.
101    pub async fn session_config(&self) -> SessionConfig {
102        self.inner.lock().await.session_config()
103    }
104
105    /// Get the [`EventEncryptionAlgorithm`] of this [`Session`].
106    #[allow(clippy::unused_async)] // The experimental-algorithms feature uses async code.
107    pub async fn algorithm(&self) -> EventEncryptionAlgorithm {
108        #[cfg(feature = "experimental-algorithms")]
109        if self.session_config().await.version() == 2 {
110            EventEncryptionAlgorithm::OlmV2Curve25519AesSha2
111        } else {
112            EventEncryptionAlgorithm::OlmV1Curve25519AesSha2
113        }
114
115        #[cfg(not(feature = "experimental-algorithms"))]
116        EventEncryptionAlgorithm::OlmV1Curve25519AesSha2
117    }
118
119    /// Encrypt the given plaintext as a OlmMessage.
120    ///
121    /// Returns the encrypted Olm message.
122    ///
123    /// # Arguments
124    ///
125    /// * `plaintext` - The plaintext that should be encrypted.
126    pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmResult<OlmMessage> {
127        let mut session = self.inner.lock().await;
128        let message = session.encrypt(plaintext)?;
129
130        self.last_use_time = SecondsSinceUnixEpoch::now();
131        debug!(?session, "Successfully encrypted an event");
132
133        Ok(message)
134    }
135
136    /// Encrypt the given event content as an m.room.encrypted event content.
137    ///
138    /// # Arguments
139    ///
140    /// - `recipient_device` - The device for which this message is going to be
141    ///   encrypted, this needs to be the device that was used to create this
142    ///   session with.
143    ///
144    /// - `event_type` - The type of the event content.
145    /// - `content` - The content of the event.
146    pub async fn encrypt(
147        &mut self,
148        recipient_device: &DeviceData,
149        event_type: &str,
150        content: impl Serialize,
151        message_id: Option<String>,
152    ) -> OlmResult<Raw<ToDeviceEncryptedEventContent>> {
153        #[derive(Debug)]
154        struct Content<'a> {
155            event_type: &'a str,
156            content: Raw<Value>,
157        }
158
159        impl EventType for Content<'_> {
160            // This is a bit of a hack: usually we just define the `EVENT_TYPE`
161            // and use the default implementation of `event_type()`. We can't do
162            // this here because the event type isn't static.
163            //
164            // We have to provide `EVENT_TYPE` to conform to the `EventType`
165            // trait, but don't actually use it, so we just leave it empty.
166            //
167            // This works because the serialization uses `event_type()` and this
168            // type is contained to this function.
169            const EVENT_TYPE: &'static str = "";
170
171            fn event_type(&self) -> &str {
172                self.event_type
173            }
174        }
175
176        impl Serialize for Content<'_> {
177            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178            where
179                S: serde::Serializer,
180            {
181                self.content.serialize(serializer)
182            }
183        }
184
185        let plaintext = {
186            let content = serde_json::to_value(content)?;
187            let content = Content { event_type, content: Raw::new(&content)? };
188
189            let recipient_signing_key =
190                recipient_device.ed25519_key().ok_or(EventError::MissingSigningKey)?;
191
192            let content = DecryptedOlmV1Event {
193                sender: self.our_device_keys.user_id.clone(),
194                recipient: recipient_device.user_id().into(),
195                keys: OlmV1Keys {
196                    ed25519: self
197                        .our_device_keys
198                        .ed25519_key()
199                        .expect("Our own device should have an Ed25519 public key"),
200                },
201                recipient_keys: OlmV1Keys { ed25519: recipient_signing_key },
202                sender_device_keys: Some(self.our_device_keys.clone()),
203                content,
204            };
205
206            serde_json::to_string(&content)?
207        };
208
209        let ciphertext = self.encrypt_helper(&plaintext).await?;
210
211        let content = self.build_encrypted_event(ciphertext, message_id).await?;
212        let content = Raw::new(&content)?;
213        Ok(content)
214    }
215
216    /// Take the given ciphertext, and package it into an `m.room.encrypted`
217    /// to-device message content.
218    ///
219    /// # Arguments
220    ///
221    /// - `ciphertext` - The encrypted message content.
222    /// - `message_id` - The ID to use for this to-device message, as
223    ///   `org.matrix.msgid`.
224    pub(crate) async fn build_encrypted_event(
225        &self,
226        ciphertext: OlmMessage,
227        message_id: Option<String>,
228    ) -> OlmResult<ToDeviceEncryptedEventContent> {
229        let content = match self.algorithm().await {
230            EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => OlmV1Curve25519AesSha2Content {
231                ciphertext,
232                recipient_key: self.sender_key,
233                sender_key: self
234                    .our_device_keys
235                    .curve25519_key()
236                    .expect("Device doesn't have curve25519 key"),
237                message_id,
238            }
239            .into(),
240            #[cfg(feature = "experimental-algorithms")]
241            EventEncryptionAlgorithm::OlmV2Curve25519AesSha2 => OlmV2Curve25519AesSha2Content {
242                ciphertext,
243                sender_key: self
244                    .our_device_keys
245                    .curve25519_key()
246                    .expect("Device doesn't have curve25519 key"),
247                message_id,
248            }
249            .into(),
250            _ => unreachable!(),
251        };
252
253        Ok(content)
254    }
255
256    /// Returns the unique identifier for this session.
257    pub fn session_id(&self) -> &str {
258        &self.session_id
259    }
260
261    /// Store the session as a base64 encoded string.
262    ///
263    /// # Arguments
264    ///
265    /// - `pickle_mode` - The mode that was used to pickle the session, either
266    ///   an unencrypted mode or an encrypted using passphrase.
267    pub async fn pickle(&self) -> PickledSession {
268        let pickle = self.inner.lock().await.pickle();
269
270        PickledSession {
271            pickle,
272            sender_key: self.sender_key,
273            created_using_fallback_key: self.created_using_fallback_key,
274            creation_time: self.creation_time,
275            last_use_time: self.last_use_time,
276        }
277    }
278
279    /// Restore a Session from a previously pickled string.
280    ///
281    /// Returns the restored Olm Session or a `SessionUnpicklingError` if there
282    /// was an error.
283    ///
284    /// # Arguments
285    ///
286    /// - `our_device_keys` - Our own signed device keys.
287    /// - `pickle` - The pickled version of the `Session`.
288    pub fn from_pickle(
289        our_device_keys: DeviceKeys,
290        pickle: PickledSession,
291    ) -> Result<Self, SessionUnpickleError> {
292        if our_device_keys.curve25519_key().is_none() {
293            return Err(SessionUnpickleError::MissingIdentityKey);
294        }
295        if our_device_keys.ed25519_key().is_none() {
296            return Err(SessionUnpickleError::MissingSigningKey);
297        }
298
299        let session: vodozemac::olm::Session = pickle.pickle.into();
300        let session_id = session.session_id();
301
302        Ok(Session {
303            inner: Arc::new(Mutex::new(session)),
304            session_id: session_id.into(),
305            created_using_fallback_key: pickle.created_using_fallback_key,
306            sender_key: pickle.sender_key,
307            our_device_keys,
308            creation_time: pickle.creation_time,
309            last_use_time: pickle.last_use_time,
310        })
311    }
312}
313
314impl PartialEq for Session {
315    fn eq(&self, other: &Self) -> bool {
316        self.session_id() == other.session_id()
317    }
318}
319
320/// A pickled version of a `Session`.
321///
322/// Holds all the information that needs to be stored in a database to restore a
323/// Session.
324#[derive(Serialize, Deserialize)]
325#[allow(missing_debug_implementations)]
326pub struct PickledSession {
327    /// The pickle string holding the Olm Session.
328    pub pickle: SessionPickle,
329    /// The curve25519 key of the other user that we share this session with.
330    pub sender_key: Curve25519PublicKey,
331    /// Was the session created using a fallback key.
332    #[serde(default)]
333    pub created_using_fallback_key: bool,
334    /// The Unix timestamp when the session was created.
335    pub creation_time: SecondsSinceUnixEpoch,
336    /// The Unix timestamp when the session was last used.
337    pub last_use_time: SecondsSinceUnixEpoch,
338}
339
340#[cfg(test)]
341mod tests {
342    use matrix_sdk_test::async_test;
343    use ruma::{device_id, user_id};
344    use serde_json::{self, Value};
345    use strass::assert_let;
346    use vodozemac::olm::{OlmMessage, SessionConfig};
347
348    use crate::{
349        identities::DeviceData,
350        olm::Account,
351        types::events::{
352            dummy::DummyEventContent, olm_v1::DecryptedOlmV1Event,
353            room::encrypted::ToDeviceEncryptedEventContent,
354        },
355    };
356
357    #[async_test]
358    async fn test_encryption_and_decryption() {
359        use ruma::events::dummy::ToDeviceDummyEventContent;
360
361        // Given users Alice and Bob
362        let alice =
363            Account::with_device_id(user_id!("@alice:localhost"), device_id!("ALICEDEVICE"));
364        let mut bob = Account::with_device_id(user_id!("@bob:localhost"), device_id!("BOBDEVICE"));
365
366        #[cfg(not(feature = "experimental-algorithms"))]
367        let config = SessionConfig::version_1();
368
369        #[cfg(feature = "experimental-algorithms")]
370        let config = SessionConfig::version_2();
371
372        // When Alice creates an Olm session with Bob
373        bob.generate_one_time_keys(1);
374        let one_time_key = *bob.one_time_keys().values().next().unwrap();
375        let sender_key = bob.identity_keys().curve25519;
376        let mut alice_session = alice
377            .create_outbound_session_helper(
378                config,
379                sender_key,
380                one_time_key,
381                false,
382                alice.device_keys(),
383            )
384            .unwrap();
385
386        let alice_device = DeviceData::from_account(&alice);
387
388        // and encrypts a message
389        let message = alice_session
390            .encrypt(&alice_device, "m.dummy", ToDeviceDummyEventContent::new(), None)
391            .await
392            .unwrap()
393            .deserialize()
394            .unwrap();
395
396        #[cfg(feature = "experimental-algorithms")]
397        assert_let!(ToDeviceEncryptedEventContent::OlmV2Curve25519AesSha2(content) = message);
398        #[cfg(not(feature = "experimental-algorithms"))]
399        assert_let!(ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(content) = message);
400
401        let OlmMessage::PreKey(prekey) = content.ciphertext else {
402            panic!("Wrong Olm message type");
403        };
404
405        // Then Bob should be able to create a session from the message and
406        // decrypt it.
407        let bob_session_result = bob
408            .create_inbound_session(
409                alice_device.curve25519_key().unwrap(),
410                bob.device_keys(),
411                &prekey,
412            )
413            .unwrap();
414
415        // Also ensure that the encrypted payload has the device keys under the
416        // stable prefix
417        let plaintext: Value = serde_json::from_str(&bob_session_result.plaintext).unwrap();
418        assert_eq!(plaintext["sender_device_keys"]["user_id"].as_str(), Some("@alice:localhost"));
419
420        // And the serialized object matches the format as specified in
421        // DecryptedOlmV1Event
422        let event: DecryptedOlmV1Event<DummyEventContent> =
423            serde_json::from_str(&bob_session_result.plaintext).unwrap();
424        assert_eq!(event.sender_device_keys.unwrap(), alice.device_keys());
425    }
426}