Skip to main content

matrix_sdk_ui/
encryption_sync_service.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 that specific language governing permissions and
13// limitations under the License.
14
15//! Encryption Sync API.
16//!
17//! The encryption sync API is a high-level helper that is designed to take care
18//! of handling the synchronization of encryption and to-device events (required
19//! for encryption), be they received within the app or within a dedicated
20//! extension process (e.g. the [NSE] process on iOS devices).
21//!
22//! Under the hood, this uses a sliding sync instance configured with no lists,
23//! but that enables the e2ee and to-device extensions, so that it can both
24//! handle encryption and manage encryption keys; that's sufficient to decrypt
25//! messages received in the notification processes.
26//!
27//! [NSE]: https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension
28
29use std::{pin::Pin, time::Duration};
30
31use async_stream::stream;
32use futures_core::stream::Stream;
33use futures_util::{StreamExt, pin_mut};
34use matrix_sdk::{Client, LEASE_DURATION_MS, SlidingSync, sleep::sleep};
35use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
36use ruma::{api::client::sync::sync_events::v5 as http, assign};
37use tokio::sync::OwnedMutexGuard;
38use tracing::{debug, instrument, trace};
39
40/// Unit type representing a permit to *use* an [`EncryptionSyncService`].
41///
42/// This must be created once in the whole application's lifetime, wrapped in a
43/// mutex. Using an `EncryptionSyncService` must then lock that mutex in an
44/// owned way, so that there's at most a single `EncryptionSyncService` running
45/// at any time in the entire app.
46pub struct EncryptionSyncPermit(());
47
48impl EncryptionSyncPermit {
49    pub(crate) fn new() -> Self {
50        Self(())
51    }
52}
53
54impl EncryptionSyncPermit {
55    /// Test-only.
56    #[doc(hidden)]
57    pub fn new_for_testing() -> Self {
58        Self::new()
59    }
60}
61
62/// High-level helper for synchronizing encryption events using sliding sync.
63///
64/// See the module's documentation for more details.
65pub struct EncryptionSyncService {
66    client: Client,
67    sliding_sync: SlidingSync,
68}
69
70impl EncryptionSyncService {
71    /// Creates a new instance of a `EncryptionSyncService`.
72    ///
73    /// This will create and manage an instance of [`matrix_sdk::SlidingSync`].
74    pub async fn new(
75        client: Client,
76        poll_and_network_timeouts: Option<(Duration, Duration)>,
77    ) -> Result<Self, Error> {
78        // Make sure to use the same `conn_id` and caching store identifier, whichever
79        // process is running this sliding sync. There must be at most one
80        // sliding sync instance that enables the e2ee and to-device extensions.
81        let mut builder = client
82            .sliding_sync("encryption")
83            .map_err(Error::SlidingSync)?
84            //.share_pos() // TODO: This is racy, needs cross-process lock :')
85            .with_to_device_extension(
86                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
87            )
88            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}));
89
90        if let Some((poll_timeout, network_timeout)) = poll_and_network_timeouts {
91            builder = builder.poll_timeout(poll_timeout).network_timeout(network_timeout);
92        }
93
94        let sliding_sync = builder.build().await.map_err(Error::SlidingSync)?;
95
96        if let CrossProcessLockConfig::MultiProcess { holder_name } =
97            client.cross_process_lock_config()
98        {
99            // Gently try to enable the cross-process lock on behalf of the user.
100            match client.encryption().enable_cross_process_store_lock(holder_name.clone()).await {
101                Ok(()) | Err(matrix_sdk::Error::BadCryptoStoreState) => {
102                    // Ignore; we've already set the crypto store lock to
103                    // something, and that's sufficient as
104                    // long as it uniquely identifies the process.
105                }
106                Err(err) => {
107                    // Any other error is fatal
108                    return Err(Error::ClientError(err));
109                }
110            }
111        }
112
113        Ok(Self { client, sliding_sync })
114    }
115
116    /// Runs an `EncryptionSyncService` loop, yielding `Ok(())` after each
117    /// iteration so the caller can decide whether to continue or stop by
118    /// dropping the stream.
119    ///
120    /// Ends without yielding if the cross-process lock is configured but
121    /// can't be acquired (another process is expected to run the sync).
122    ///
123    /// Note: the [`EncryptionSyncPermit`] parameter ensures that there's at
124    /// most one encryption sync running at any time. See its documentation
125    /// for more details.
126    pub fn run_iterations(
127        self,
128        permit: OwnedMutexGuard<EncryptionSyncPermit>,
129    ) -> impl Stream<Item = Result<(), Error>> {
130        stream!({
131            // Move the permit into the stream, so that it's held for as long as the stream
132            // is alive.
133            let _permit = permit;
134
135            let _lock_guard = if let CrossProcessLockConfig::MultiProcess { .. } =
136                self.client.cross_process_lock_config()
137            {
138                let mut lock_guard = match self.client.encryption().try_lock_store_once().await {
139                    Ok(lock_guard) => lock_guard,
140                    Err(err) => {
141                        yield Err(Error::LockError(err));
142                        return;
143                    }
144                };
145
146                // Try to take the lock at the beginning; if it's busy, that means that another
147                // process already holds onto it, and as such we won't try to run the
148                // encryption sync loop at all (because we expect the other process to
149                // do so).
150
151                if lock_guard.is_none() {
152                    tracing::debug!(
153                        "Lock was already taken, and we're not the main loop; retrying in {}ms...",
154                        LEASE_DURATION_MS
155                    );
156
157                    sleep(Duration::from_millis(LEASE_DURATION_MS.into())).await;
158
159                    lock_guard = match self.client.encryption().try_lock_store_once().await {
160                        Ok(lock_guard) => lock_guard,
161                        Err(err) => {
162                            yield Err(Error::LockError(err));
163                            return;
164                        }
165                    };
166
167                    if lock_guard.is_none() {
168                        tracing::debug!(
169                            "Second attempt at locking outside the main app failed, aborting."
170                        );
171                        return;
172                    }
173                }
174
175                lock_guard
176            } else {
177                None
178            };
179
180            let sync = self.sliding_sync.sync();
181
182            pin_mut!(sync);
183
184            loop {
185                match sync.next().await {
186                    Some(Ok(update_summary)) => {
187                        // This API is only concerned with the e2ee and to-device extensions.
188                        // Warn if anything weird has been received from the homeserver.
189                        if !update_summary.lists.is_empty() {
190                            debug!(?update_summary.lists, "unexpected non-empty list of lists in encryption sync API");
191                        }
192                        if !update_summary.rooms.is_empty() {
193                            debug!(?update_summary.rooms, "unexpected non-empty list of rooms in encryption sync API");
194                        }
195
196                        // Cool cool, let's do it again.
197                        trace!("Encryption sync received an update!");
198                        yield Ok(());
199                    }
200
201                    Some(Err(err)) => {
202                        trace!("Encryption sync stopped because of an error: {err:#}");
203                        yield Err(Error::SlidingSync(err));
204                        break;
205                    }
206
207                    None => {
208                        trace!("Encryption sync properly terminated.");
209                        break;
210                    }
211                }
212            }
213        })
214    }
215
216    /// Start synchronization.
217    ///
218    /// This should be regularly polled.
219    ///
220    /// Note: the [`EncryptionSyncPermit`] parameter ensures that there's at
221    /// most one encryption sync running at any time. See its documentation
222    /// for more details.
223    #[doc(hidden)] // Only public for testing purposes.
224    pub fn sync(
225        &self,
226        permit: OwnedMutexGuard<EncryptionSyncPermit>,
227    ) -> impl Stream<Item = Result<(), Error>> + '_ {
228        stream!({
229            // Move the permit into the stream, so that it's held for as long as the stream
230            // is alive.
231            let _permit = permit;
232
233            let sync = self.sliding_sync.sync();
234
235            pin_mut!(sync);
236
237            loop {
238                match self.next_sync_with_lock(&mut sync).await? {
239                    Some(Ok(update_summary)) => {
240                        // This API is only concerned with the e2ee and to-device extensions.
241                        // Warn if anything weird has been received from the homeserver.
242                        if !update_summary.lists.is_empty() {
243                            debug!(?update_summary.lists, "unexpected non-empty list of lists in encryption sync API");
244                        }
245                        if !update_summary.rooms.is_empty() {
246                            debug!(?update_summary.rooms, "unexpected non-empty list of rooms in encryption sync API");
247                        }
248
249                        // Cool cool, let's do it again.
250                        trace!("Encryption sync received an update!");
251                        yield Ok(());
252                        continue;
253                    }
254
255                    Some(Err(err)) => {
256                        trace!("Encryption sync stopped because of an error: {err:#}");
257                        yield Err(Error::SlidingSync(err));
258                        break;
259                    }
260
261                    None => {
262                        trace!("Encryption sync properly terminated.");
263                        break;
264                    }
265                }
266            }
267        })
268    }
269
270    /// Helper function for `sync`. Take the cross-process store lock, and call
271    /// `sync.next()`
272    #[instrument(skip_all)]
273    async fn next_sync_with_lock<Item>(
274        &self,
275        sync: &mut Pin<&mut impl Stream<Item = Item>>,
276    ) -> Result<Option<Item>, Error> {
277        let _guard = if let CrossProcessLockConfig::MultiProcess { .. } =
278            self.client.cross_process_lock_config()
279        {
280            self.client.encryption().spin_lock_store(Some(60000)).await.map_err(Error::LockError)?
281        } else {
282            None
283        };
284
285        Ok(sync.next().await)
286    }
287
288    /// Requests that the underlying sliding sync be stopped.
289    ///
290    /// This will unlock the cross-process lock, if taken.
291    pub(crate) fn stop_sync(&self) -> Result<(), Error> {
292        // Stopping the sync loop will cause the next `next()` call to return `None`, so
293        // this will also release the cross-process lock automatically.
294        self.sliding_sync.stop_sync().map_err(Error::SlidingSync)?;
295
296        Ok(())
297    }
298
299    pub(crate) async fn expire_sync_session(&self) {
300        self.sliding_sync.expire_session().await;
301    }
302}
303
304/// Errors for the [`EncryptionSyncService`].
305#[derive(Debug, thiserror::Error)]
306pub enum Error {
307    #[error("Something wrong happened in sliding sync: {0:#}")]
308    SlidingSync(matrix_sdk::Error),
309
310    #[error("Locking failed: {0:#}")]
311    LockError(matrix_sdk::Error),
312
313    #[error(transparent)]
314    ClientError(matrix_sdk::Error),
315}