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