Skip to main content

matrix_sdk/encryption/secret_storage/
secret_store.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
15use std::fmt;
16
17use matrix_sdk_base::crypto::{CrossSigningKeyExport, secret_storage::SecretStorageKey};
18use ruma::{
19    events::{
20        GlobalAccountDataEventType, secret::request::SecretName,
21        secret_storage::secret::SecretEventContent,
22    },
23    serde::Raw,
24};
25use serde_json::value::to_raw_value;
26use tracing::{
27    Span, error,
28    field::{debug, display},
29    info, instrument, warn,
30};
31use zeroize::Zeroize;
32
33use super::{DecryptionError, Result, SecretStorageError};
34use crate::{Client, encryption::backups::EnableBackupError};
35
36#[cfg_attr(doc, doc = include_str!("../../../.cargo/mermaid.html"))]
37/// Secure key/value storage for Matrix users.
38///
39/// The `SecretStore` struct encapsulates the secret storage mechanism for
40/// Matrix users, as it is specified in the [Matrix specification].
41///
42/// This specialized storage is tied to the user's Matrix account and serves as
43/// an encrypted key/value store, backed by [account data] residing on the
44/// homeserver. Any secrets uploaded to the homeserver using the
45/// [`SecretStore::put_secret()`] method are automatically encrypted by the
46/// [`SecretStore`].
47///
48/// [`SecretStore`] enables you to safely manage and access sensitive
49/// information while ensuring that it remains protected from unauthorized
50/// access. It plays a crucial role in maintaining the privacy and security of a
51/// Matrix user's data.
52///
53/// **Data Flow Overview:**
54///
55/// ```mermaid
56/// flowchart LR
57///    subgraph Client
58///        SecretStore
59///    end
60///    subgraph Homeserver
61///        data[Account Data]
62///    end
63///    SecretStore <== Encrypted ==> data
64/// ```
65///
66/// **Note**: It's important to emphasize that the `SecretStore` should not be
67/// used for storing large volumes of data due to its nature as a key/value
68/// store for sensitive information.
69///
70/// # Examples
71///
72/// ```no_run
73/// # use matrix_sdk::Client;
74/// # use url::Url;
75/// # async {
76/// # let homeserver = Url::parse("http://example.com")?;
77/// # let client = Client::new(homeserver).await?;
78/// use ruma::events::secret::request::SecretName;
79///
80/// let secret_store = client
81///     .encryption()
82///     .secret_storage()
83///     .open_secret_store("It's a secret to everybody")
84///     .await?;
85///
86/// let my_secret = "Top secret secret";
87/// let my_secret_name = SecretName::from("m.treasure");
88///
89/// secret_store.put_secret(my_secret_name, my_secret);
90///
91/// # anyhow::Ok(()) };
92/// ```
93///
94/// [Matrix specification]: https://spec.matrix.org/v1.8/client-server-api/#secret-storage
95/// [account data]: https://spec.matrix.org/v1.8/client-server-api/#client-config
96pub struct SecretStore {
97    pub(super) client: Client,
98    pub(super) key: SecretStorageKey,
99}
100
101impl SecretStore {
102    /// Export the [`SecretStorageKey`] of this [`SecretStore`] as a
103    /// base58-encoded string as defined in the [spec].
104    ///
105    /// _Note_: This returns a copy of the private key material of the
106    /// [`SecretStorageKey`] as a string. The caller needs to ensure that this
107    /// string is zeroized.
108    ///
109    /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#key-representation
110    pub fn secret_storage_key(&self) -> String {
111        self.key.to_base58()
112    }
113
114    /// Retrieve a secret from the homeserver's account data
115    ///
116    /// This method allows you to retrieve a secret from the account data stored
117    /// on the Matrix homeserver.
118    ///
119    /// # Arguments
120    ///
121    /// - `secret_name`: The name of the secret. The provided `secret_name`
122    ///   serves as the event type for the associated account data event.
123    ///
124    /// The `retrieve_secret` method enables you to access and decrypt secrets
125    /// previously stored in the user's account data on the homeserver. You can
126    /// use the `secret_name` parameter to specify the desired secret to
127    /// retrieve.
128    ///
129    /// # Examples
130    ///
131    /// ```no_run
132    /// # use matrix_sdk::Client;
133    /// # use url::Url;
134    /// # async {
135    /// # let homeserver = Url::parse("http://example.com")?;
136    /// # let client = Client::new(homeserver).await?;
137    /// use ruma::events::secret::request::SecretName;
138    ///
139    /// let secret_store = client
140    ///     .encryption()
141    ///     .secret_storage()
142    ///     .open_secret_store("It's a secret to everybody")
143    ///     .await?;
144    ///
145    /// let my_secret_name = SecretName::from("m.treasure");
146    ///
147    /// let secret = secret_store.get_secret(my_secret_name).await?;
148    ///
149    /// # anyhow::Ok(()) };
150    /// ```
151    pub async fn get_secret(&self, secret_name: impl Into<SecretName>) -> Result<Option<String>> {
152        let secret_name = secret_name.into();
153        let event_type = GlobalAccountDataEventType::from(secret_name.to_owned());
154
155        if let Some(secret_content) = self
156            .client
157            .account()
158            .fetch_account_data(event_type)
159            .await
160            .map_err(|e| SecretStorageError::into_import_error(secret_name.clone(), e))?
161        {
162            let mut secret_content = secret_content
163                .deserialize_as_unchecked::<SecretEventContent>()
164                .map_err(|e| SecretStorageError::into_import_error(secret_name.clone(), e))?;
165
166            // The `SecretEventContent` contains a map from the secret storage
167            // key ID to the ciphertext. Let's try to find a secret which was
168            // encrypted using our [`SecretStorageKey`].
169            if let Some(secret_content) = secret_content.encrypted.remove(self.key.key_id()) {
170                // We found a secret we should be able to decrypt, let's try to
171                // do so.
172                let decrypted = self
173                    .key
174                    .decrypt(
175                        &secret_content.deserialize_as().map_err(|e| {
176                            SecretStorageError::into_import_error(secret_name.clone(), e)
177                        })?,
178                        &secret_name,
179                    )
180                    .map_err(DecryptionError::from)
181                    .map_err(|e| SecretStorageError::into_import_error(secret_name.clone(), e))?;
182
183                let secret = String::from_utf8(decrypted)
184                    .map_err(DecryptionError::from)
185                    .map_err(|e| SecretStorageError::into_import_error(secret_name.clone(), e))?;
186
187                Ok(Some(secret))
188            } else {
189                // We did not find a secret which was encrypted using our
190                // [`SecretStorageKey`], no need to try to decrypt.
191                Ok(None)
192            }
193        } else {
194            Ok(None)
195        }
196    }
197
198    /// Store a secret in the homeserver's account data
199    ///
200    /// This method allows you to securely store a secret on the Matrix
201    /// homeserver as an encrypted account data event.
202    ///
203    /// # Arguments
204    ///
205    /// - `secret_name`: The name of the secret. The provided `secret_name`
206    ///   serves as the event type for the account data event on the homeserver.
207    ///
208    /// - `secret`: The secret to be stored on the homeserver. The secret is
209    ///   encrypted before being stored, ensuring its confidentiality and
210    ///   integrity.
211    ///
212    /// # Examples
213    ///
214    /// ```no_run
215    /// # use matrix_sdk::Client;
216    /// # use url::Url;
217    /// # async {
218    /// # let homeserver = Url::parse("http://example.com")?;
219    /// # let client = Client::new(homeserver).await?;
220    /// use ruma::events::secret::request::SecretName;
221    ///
222    /// let secret_store = client
223    ///     .encryption()
224    ///     .secret_storage()
225    ///     .open_secret_store("It's a secret to everybody")
226    ///     .await?;
227    ///
228    /// let my_secret = "Top secret secret";
229    /// let my_secret_name = SecretName::from("m.treasure");
230    ///
231    /// secret_store.put_secret(my_secret_name, my_secret);
232    ///
233    /// # anyhow::Ok(()) };
234    /// ```
235    pub async fn put_secret(&self, secret_name: impl Into<SecretName>, secret: &str) -> Result<()> {
236        // This function does a read/update/store of an account data event
237        // stored on the homeserver. We first fetch the existing account data
238        // event, the event contains a map which gets updated by this method,
239        // finally we upload the modified event.
240        //
241        // To prevent multiple calls to this method trying to update a secret at
242        // the same time, and thus trampling on each other we introduce a lock
243        // which acts as a semaphore.
244        //
245        // Technically there's a low chance of this happening since we're not
246        // storing many secrets and the bigger problem is that another client
247        // might be doing this as well and the server doesn't have a mechanism
248        // to protect against this.
249        //
250        // We could make this lock be per `secret_name` but this is not a
251        // performance critical method.
252        let _guard = self.client.locks().store_secret_lock.lock().await;
253
254        let secret_name = secret_name.into();
255        let event_type = GlobalAccountDataEventType::from(secret_name.to_owned());
256
257        // Get the existing account data event or create a new empty one.
258        let mut secret_content = if let Some(secret_content) =
259            self.client.account().fetch_account_data(event_type.to_owned()).await?
260        {
261            secret_content
262                .deserialize_as_unchecked::<SecretEventContent>()
263                .unwrap_or_else(|_| SecretEventContent::new(Default::default()))
264        } else {
265            SecretEventContent::new(Default::default())
266        };
267
268        // Encrypt the secret.
269        let secret = secret.as_bytes().to_vec();
270        let encrypted_secret = self.key.encrypt(secret, &secret_name);
271
272        // Insert the encrypted secret into the account data event.
273        secret_content
274            .encrypted
275            .insert(self.key.key_id().to_owned(), Raw::new(&encrypted_secret)?.cast());
276        let secret_content = Raw::from_json(to_raw_value(&secret_content)?);
277
278        // Upload the modified account data event, now that the new secret has
279        // been inserted.
280        self.client.account().set_account_data_raw(event_type, secret_content).await?;
281
282        Ok(())
283    }
284
285    /// Get all the well-known private parts/keys of the [`OwnUserIdentity`] as
286    /// a [`CrossSigningKeyExport`].
287    ///
288    /// The export can be imported into the [`OlmMachine`] using
289    /// [`OlmMachine::import_cross_signing_keys()`].
290    async fn get_cross_signing_keys(&self) -> Result<CrossSigningKeyExport> {
291        let mut export = CrossSigningKeyExport::default();
292
293        export.master_key = self.get_secret(SecretName::CrossSigningMasterKey).await?;
294        export.self_signing_key = self.get_secret(SecretName::CrossSigningSelfSigningKey).await?;
295        export.user_signing_key = self.get_secret(SecretName::CrossSigningUserSigningKey).await?;
296
297        Ok(export)
298    }
299
300    async fn put_cross_signing_keys(&self, export: CrossSigningKeyExport) -> Result<()> {
301        if let Some(master_key) = &export.master_key {
302            self.put_secret(SecretName::CrossSigningMasterKey, master_key).await?;
303        }
304
305        if let Some(user_signing_key) = &export.user_signing_key {
306            self.put_secret(SecretName::CrossSigningUserSigningKey, user_signing_key).await?;
307        }
308
309        if let Some(self_signing_key) = &export.self_signing_key {
310            self.put_secret(SecretName::CrossSigningSelfSigningKey, self_signing_key).await?;
311        }
312
313        Ok(())
314    }
315
316    async fn maybe_enable_backups(&self) -> Result<()> {
317        match self.get_secret(SecretName::RecoveryKey).await {
318            Ok(Some(mut secret)) => {
319                let ret = self
320                    .client
321                    .encryption()
322                    .backups()
323                    .maybe_enable_backups(&secret)
324                    .await
325                    .map_err(|e| match e {
326                        EnableBackupError::InconsistentBackupDecryptionKey => {
327                            SecretStorageError::InconsistentBackupDecryptionKey
328                        }
329                        EnableBackupError::Error(error) => {
330                            SecretStorageError::into_import_error(SecretName::RecoveryKey, error)
331                        }
332                    });
333
334                if let Err(e) = &ret {
335                    warn!("Could not enable backups from secret storage: {e:?}");
336                }
337
338                secret.zeroize();
339
340                Ok(ret.map(|_| ())?)
341            }
342            Err(e) => {
343                warn!("Could not enable backups from secret storage: {e:?}");
344                Err(SecretStorageError::MissingOrInvalidBackupDecryptionKey)
345            }
346            Ok(None) => {
347                info!("No backup recovery key found.");
348
349                Ok(())
350            }
351        }
352    }
353
354    /// Retrieve and store well-known secrets locally
355    ///
356    /// This method retrieves and stores all well-known secrets from the account
357    /// data on the Matrix homeserver to enhance local security and identity
358    /// verification.
359    ///
360    /// The following secrets are retrieved by this method:
361    ///
362    /// - `m.cross_signing.master`: The master cross-signing key.
363    /// - `m.cross_signing.self_signing`: The self-signing cross-signing key.
364    /// - `m.cross_signing.user_signing`: The user-signing cross-signing key.
365    /// - `m.megolm_backup.v1`: The backup recovery key.
366    ///
367    /// If the `m.cross_signing.self_signing` key is successfully imported, it
368    /// is used to sign our own [`Device`], marking it as verified. This step is
369    /// establishes trust in your own device's identity.
370    ///
371    /// By invoking this method, you ensure that your device has access to the
372    /// necessary secrets for device and identity verification.
373    ///
374    /// # Examples
375    ///
376    /// ```no_run
377    /// # use matrix_sdk::Client;
378    /// # use url::Url;
379    /// # async {
380    /// # let homeserver = Url::parse("http://example.com")?;
381    /// # let client = Client::new(homeserver).await?;
382    /// use ruma::events::secret::request::SecretName;
383    ///
384    /// let secret_store = client
385    ///     .encryption()
386    ///     .secret_storage()
387    ///     .open_secret_store("It's a secret to everybody")
388    ///     .await?;
389    ///
390    /// secret_store.import_secrets().await?;
391    ///
392    /// let status = client
393    ///     .encryption()
394    ///     .cross_signing_status()
395    ///     .await
396    ///     .expect("We should be able to check out cross-signing status");
397    ///
398    /// println!("Cross-signing status {status:?}");
399    ///
400    /// # anyhow::Ok(()) };
401    /// ```
402    ///
403    /// [`Device`]: crate::encryption::identities::Device
404    #[instrument(fields(user_id, device_id, cross_signing_status))]
405    pub async fn import_secrets(&self) -> Result<()> {
406        let olm_machine = self.client.olm_machine().await;
407        let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
408
409        Span::current()
410            .record("user_id", display(olm_machine.user_id()))
411            .record("device_id", display(olm_machine.device_id()));
412
413        info!("Fetching the private cross-signing keys from the secret store");
414
415        // Get all our private cross-signing keys from the secret store.
416        let export = self.get_cross_signing_keys().await?;
417
418        info!(cross_signing_keys = ?export, "Received the cross signing keys from the server");
419
420        // We need to ensure that we have the public parts of the cross-signing
421        // keys, those are represented as the `OwnUserIdentity` struct. The
422        // public parts from the server are compared to the public parts
423        // re-derived from the private parts. We will only import the private
424        // parts of the cross-signing keys if they match to the public parts,
425        // otherwise we would risk importing some stale cross-signing keys
426        // leftover in the secret store.
427        let (request_id, request) = olm_machine.query_keys_for_users([olm_machine.user_id()]);
428        self.client.keys_query(&request_id, request.device_keys).await?;
429
430        // Let's now try to import our private cross-signing keys.
431        let status = olm_machine
432            .import_cross_signing_keys(export)
433            .await
434            .map_err(SecretStorageError::from_secret_import_error)?;
435
436        Span::current().record("cross_signing_status", debug(&status));
437
438        info!("Done importing the cross signing keys");
439
440        if status.has_self_signing {
441            info!("Successfully imported the self-signing key, attempting to sign our own device");
442
443            // Now that we successfully imported them, the self-signing key can
444            // be used to verify our own device so other devices and user
445            // identities trust it if the trust our user identity.
446            if let Some(own_device) = self.client.encryption().get_own_device().await? {
447                own_device.verify().await?;
448
449                // Another /keys/query request to ensure that the signatures we
450                // uploaded using `own_device.verify()` are attached to the
451                // `Device` we have in storage.
452                let (request_id, request) =
453                    olm_machine.query_keys_for_users([olm_machine.user_id()]);
454                self.client.keys_query(&request_id, request.device_keys).await?;
455
456                info!("Successfully signed our own device, the device is now verified");
457            } else {
458                error!("Couldn't find our own device in the store");
459            }
460        }
461
462        self.maybe_enable_backups().await?;
463
464        Ok(())
465    }
466
467    /// Upload well-known secrets to the server
468    ///
469    /// This method uploads all well-known secrets to the account data on the
470    /// Matrix homeserver.
471    ///
472    /// The following secrets are uploaded by this method:
473    ///
474    /// - `m.cross_signing.master`: The master cross-signing key.
475    /// - `m.cross_signing.self_signing`: The self-signing cross-signing key.
476    /// - `m.cross_signing.user_signing`: The user-signing cross-signing key.
477    /// - `m.megolm_backup.v1`: The backup recovery key.
478    ///
479    /// By invoking this method, you ensure that the account data holds a copy
480    /// of the necessary secrets for device and identity verification and key
481    /// storage (if enabled).
482    pub async fn export_secrets(&self) -> Result<()> {
483        let olm_machine = self.client.olm_machine().await;
484        let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
485
486        if let Some(cross_signing_keys) = olm_machine.export_cross_signing_keys().await? {
487            self.put_cross_signing_keys(cross_signing_keys).await?;
488        }
489
490        let backup_keys = olm_machine.backup_machine().get_backup_keys().await?;
491
492        if let Some(backup_recovery_key) = backup_keys.decryption_key {
493            let mut key = backup_recovery_key.to_base64();
494            self.put_secret(SecretName::RecoveryKey, &key).await?;
495
496            key.zeroize();
497        }
498
499        Ok(())
500    }
501}
502
503impl fmt::Debug for SecretStore {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        f.debug_struct("SecretStore").field("key", &self.key).finish_non_exhaustive()
506    }
507}