diff --git a/Cargo.lock b/Cargo.lock index 6c9e439f1..0b8931d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,20 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "aquamarine" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df752953c49ce90719c7bf1fc587bc8227aed04732ea0c0f85e5397d7fdbd1a1" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -2474,6 +2488,25 @@ dependencies = [ "bitmaps", ] +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indenter" version = "0.3.3" @@ -3036,6 +3069,7 @@ version = "0.6.2" dependencies = [ "anyhow", "anymap2", + "aquamarine", "as_variant", "assert-json-diff", "assert_matches", @@ -3943,9 +3977,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "3.9.1" +version = "3.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" dependencies = [ "num-traits", ] @@ -4433,6 +4467,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", + "syn 1.0.109", "version_check", ] diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index c90671be3..11178a370 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -33,6 +33,10 @@ Bug fixes: Additions: +- Add secret storage support, the secret store can be opened using the + `Client::encryption()::open_secret_store()` method, which allows you to import + or export secrets from the account-data backed secret-store. + - Add `VerificationRequest::state` and `VerificationRequest::changes` to check and listen to changes in the state of the `VerificationRequest`. This removes the need to listen to individual matrix events once the `VerificationRequest` diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 064e701ed..2d00437f6 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -62,6 +62,7 @@ docsrs = ["e2e-encryption", "sqlite", "sso-login", "qrcode", "image-proc"] [dependencies] anyhow = { workspace = true, optional = true } anymap2 = "0.13.0" +aquamarine = "0.3.2" as_variant = { workspace = true } async-channel = "1.9.0" async-stream = { workspace = true } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 3d4f94645..3a5832923 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -142,9 +142,24 @@ pub struct Client { #[derive(Default)] pub(crate) struct ClientLocks { /// Lock ensuring that only a single room may be marked as a DM at once. - /// Look at the [`Room::mark_as_dm()`] method for a more detailed + /// Look at the [`Account::mark_as_dm()`] method for a more detailed /// explanation. pub(crate) mark_as_dm_lock: Mutex<()>, + /// Lock ensuring that only a single secret store is getting opened at the + /// same time. + /// + /// This is important so we don't accidentally create multiple different new + /// default secret storage keys. + #[cfg(feature = "e2e-encryption")] + pub(crate) open_secret_store_lock: Mutex<()>, + /// Lock ensuring that we're only storing a single secret at a time. + /// + /// Take a look at the [`SecretStore::put_secret`] method for a more + /// detailed explanation. + /// + /// [`SecretStore::put_secret`]: crate::encryption::secret_storage::SecretStore::put_secret + #[cfg(feature = "e2e-encryption")] + pub(crate) store_secret_lock: Mutex<()>, /// Handler making sure we only have one group session sharing request in /// flight per room. #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index af5f57f21..9d5c96da7 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -74,6 +74,7 @@ use crate::{ pub mod futures; pub mod identities; +pub mod secret_storage; pub mod verification; pub use matrix_sdk_base::crypto::{ @@ -86,6 +87,7 @@ pub use matrix_sdk_base::crypto::{ SessionCreationError, SignatureError, VERSION, }; +use self::secret_storage::SecretStorage; pub use crate::error::RoomKeyImportError; /// Settings for end-to-end encryption features. @@ -1055,6 +1057,11 @@ impl Encryption { Ok(olm.import_room_keys(import, false, |_, _| {}).await?) } + /// Get the secret storage manager of the client. + pub fn secret_storage(&self) -> SecretStorage { + SecretStorage { client: self.client.to_owned() } + } + /// Enables the crypto-store cross-process lock. /// /// This may be required if there are multiple processes that may do writes diff --git a/crates/matrix-sdk/src/encryption/secret_storage/futures.rs b/crates/matrix-sdk/src/encryption/secret_storage/futures.rs new file mode 100644 index 000000000..33466bed5 --- /dev/null +++ b/crates/matrix-sdk/src/encryption/secret_storage/futures.rs @@ -0,0 +1,81 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{future::IntoFuture, pin::Pin}; + +use futures_core::Future; +use matrix_sdk_base::crypto::secret_storage::SecretStorageKey; +use ruma::events::secret_storage::default_key::SecretStorageDefaultKeyEventContent; + +use super::{Result, SecretStorage, SecretStore}; + +/// Future returned by [`SecretStorage::create_secret_store()`]. +#[derive(Debug)] +pub struct CreateStore<'a> { + pub(super) secret_storage: &'a SecretStorage, + pub(super) passphrase: Option<&'a str>, +} + +impl<'a> CreateStore<'a> { + /// Set the passphrase for the new [`SecretStore`]. + /// + /// See the documentation for the [`SecretStorage::create_secret_store()`] + /// method for more info. + pub fn with_passphrase(mut self, passphrase: &'a str) -> Self { + self.passphrase = Some(passphrase); + + self + } +} + +impl<'a> IntoFuture for CreateStore<'a> { + type Output = Result; + #[cfg(target_arch = "wasm32")] + type IntoFuture = Pin + 'a>>; + #[cfg(not(target_arch = "wasm32"))] + type IntoFuture = Pin + Send + 'a>>; + + fn into_future(self) -> Self::IntoFuture { + let Self { secret_storage, passphrase } = self; + + Box::pin(async move { + // Prevent multiple simultaneous calls to this method. + // + // See the documentation for the lock in the `store_secret` method for more + // info. + let client_copy = secret_storage.client.to_owned(); + let _guard = client_copy.locks().open_secret_store_lock.lock().await; + + let new_key = if let Some(passphrase) = passphrase { + SecretStorageKey::new_from_passphrase(passphrase) + } else { + SecretStorageKey::new() + }; + + let content = new_key.event_content().to_owned(); + + secret_storage.client.account().set_account_data(content).await?; + + let store = SecretStore { client: secret_storage.client.to_owned(), key: new_key }; + store.export_secrets().await?; + + let default_key_content = + SecretStorageDefaultKeyEventContent::new(store.key.key_id().to_owned()); + + store.client.account().set_account_data(default_key_content).await?; + + Ok(store) + }) + } +} diff --git a/crates/matrix-sdk/src/encryption/secret_storage/mod.rs b/crates/matrix-sdk/src/encryption/secret_storage/mod.rs new file mode 100644 index 000000000..93fe3bbd5 --- /dev/null +++ b/crates/matrix-sdk/src/encryption/secret_storage/mod.rs @@ -0,0 +1,288 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Secret Storage Support +//! +//! This submodule provides essential functionality for secret storage in +//! compliance with the [Matrix protocol specification][spec]. +//! +//! Secret storage is a critical component that provides an encrypted +//! key/value storage system. It leverages [account data] events stored on the +//! Matrix homeserver to ensure secure and private storage of sensitive +//! information. +//! +//! For detailed information and usage guidelines, refer to the documentation of +//! the [`SecretStore`] struct. +//! +//! # Examples +//! +//! ```no_run +//! # use matrix_sdk::Client; +//! # use url::Url; +//! # async { +//! # let homeserver = Url::parse("http://example.com")?; +//! # let client = Client::new(homeserver).await?; +//! use ruma::events::secret::request::SecretName; +//! +//! // Open the store. +//! let secret_store = client +//! .encryption() +//! .secret_storage() +//! .open_secret_store("It's a secret to everybody") +//! .await?; +//! +//! // Import the secrets. +//! secret_store.import_secrets().await?; +//! +//! // Our own device should now be verified. +//! let device = client +//! .encryption() +//! .get_own_device() +//! .await? +//! .expect("We should be able to retrieve our own device"); +//! +//! assert!(device.is_cross_signed_by_owner()); +//! +//! # anyhow::Ok(()) }; +//! ``` +//! +//! [spec]: https://spec.matrix.org/v1.8/client-server-api/#secret-storage +//! [account data]: https://spec.matrix.org/v1.8/client-server-api/#client-config + +use std::string::FromUtf8Error; + +use matrix_sdk_base::crypto::{ + secret_storage::{DecodeError, MacError, SecretStorageKey}, + CryptoStoreError, SecretImportError, +}; +use ruma::events::{ + secret_storage::{ + default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent, + }, + EventContentFromType, GlobalAccountDataEventType, +}; +use serde_json::value::to_raw_value; +use thiserror::Error; + +use super::identities::ManualVerifyError; +use crate::Client; + +mod futures; +mod secret_store; + +pub use futures::CreateStore; +pub use secret_store::SecretStore; + +/// Convenicence type alias for the secret-storage specific results. +pub type Result = std::result::Result; + +/// Error type for the secret-storage subsystem. +#[derive(Debug, Error)] +pub enum SecretStorageError { + /// A typical SDK error. + #[error(transparent)] + Sdk(#[from] crate::Error), + + /// Error when deserializing account data events. + #[error(transparent)] + Json(#[from] serde_json::Error), + + /// The secret storage key could not have been decoded or verified + /// successfully. + #[error(transparent)] + SecretStorageKey(#[from] DecodeError), + + /// The secret store could not be opened because info about the + /// secret-storage key could not have been found in the account data of + /// the user. + #[error( + "The info about the secret key could not have been found in the account data of the user" + )] + MissingKeyInfo { + /// The key ID of the default key. Will be set to the key ID in the + /// `m.secret_storage.default_key` event. If the + /// `m.secret_storage.default_key` does not exits, will be + /// `None`. + key_id: Option, + }, + + /// A secret could not have been imported from the secret store into the + /// local store. + #[error(transparent)] + SecretImportError(#[from] SecretImportError), + + /// A general storage error. + #[error(transparent)] + Storage(#[from] CryptoStoreError), + + /// An error happened while trying to mark our own device as verified after + /// the private cross-signing keys have been imported. + #[error(transparent)] + Verification(#[from] ManualVerifyError), + + /// Error describing a decryption failure of a secret. + #[error(transparent)] + Decryption(#[from] DecryptionError), +} + +/// Error type describing decryption failures of the secret-storage system. +#[derive(Debug, Error)] +pub enum DecryptionError { + /// The secret could not have been decrypted. + #[error("Could not decrypt the secret using the secret storage key, invalid MAC.")] + Mac(#[from] MacError), + + /// Could not decode the secret, the secret is not valid UTF-8. + #[error("Could not decode the secret, the secret is not valid UTF-8")] + Utf8(#[from] FromUtf8Error), +} + +/// A high-level API to manage secret storage. +/// +/// To get this, use [`Client::encryption()::secret_storage()`]. +#[derive(Debug)] +pub struct SecretStorage { + pub(super) client: Client, +} + +impl SecretStorage { + /// Open the [`SecretStore`] with the given `key`. + /// + /// The `secret_storage_key` can be a passphrase or a Base58 encoded secret + /// storage key. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// use ruma::events::secret::request::SecretName; + /// + /// let secret_store = client + /// .encryption() + /// .secret_storage() + /// .open_secret_store("It's a secret to everybody") + /// .await?; + /// + /// let my_secret = "Top secret secret"; + /// let my_secret_name = "m.treasure"; + /// + /// secret_store.put_secret(my_secret_name, my_secret); + /// + /// # anyhow::Ok(()) }; + /// ``` + pub async fn open_secret_store(&self, secret_storage_key: &str) -> Result { + let maybe_default_key_id = self + .client + .account() + .fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey) + .await?; + + if let Some(default_key_id) = maybe_default_key_id { + let default_key_id = + default_key_id.deserialize_as::()?; + + let event_type = + GlobalAccountDataEventType::SecretStorageKey(default_key_id.key_id.to_owned()); + let secret_key = + self.client.account().fetch_account_data(event_type.to_owned()).await?; + + if let Some(secret_key_content) = secret_key { + let event_type = event_type.to_string(); + let secret_key_content = to_raw_value(&secret_key_content)?; + + let secret_key_content = + SecretStorageKeyEventContent::from_parts(&event_type, &secret_key_content)?; + + let key = + SecretStorageKey::from_account_data(secret_storage_key, secret_key_content)?; + + Ok(SecretStore { client: self.client.to_owned(), key }) + } else { + Err(SecretStorageError::MissingKeyInfo { key_id: Some(default_key_id.key_id) }) + } + } else { + Err(SecretStorageError::MissingKeyInfo { key_id: None }) + } + } + + /// Create a new [`SecretStore`]. + /// + /// The [`SecretStore`] will be protected by a randomly generated key, or + /// optionally a passphrase can be provided as well. + /// + /// In both cases, whether a passphrase was provided or not, the key to open + /// the [`SecretStore`] can be obtained using the + /// [`SecretStore::secret_storage_key()`] method. + /// + /// *Note*: This method will set the new secret storage key as the default + /// key in the `m.secret_storage.default_key` event. All the known secrets + /// will be re-encrypted and uploaded to the homeserver as well. This + /// includes the following secrets: + /// + /// - `m.cross_signing.master`: The master cross-signing key. + /// - `m.cross_signing.self_signing`: The self-signing cross-signing key. + /// - `m.cross_signing.user_signing`: The user-signing cross-signing key. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// use ruma::events::secret::request::SecretName; + /// + /// let secret_store = client + /// .encryption() + /// .secret_storage() + /// .create_secret_store() + /// .await?; + /// + /// let my_secret = "Top secret secret"; + /// let my_secret_name = SecretName::from("m.treasure"); + /// + /// secret_store.put_secret(my_secret_name, my_secret); + /// + /// let secret_storage_key = secret_store.secret_storage_key(); + /// + /// println!("Your secret storage key is {secret_storage_key}, save it somewhere safe."); + /// + /// # anyhow::Ok(()) }; + /// ``` + pub fn create_secret_store(&self) -> CreateStore<'_> { + CreateStore { secret_storage: self, passphrase: None } + } + + /// Is secret storage set up for this user? + pub async fn is_enabled(&self) -> crate::Result { + if let Some(content) = self + .client + .account() + .fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey) + .await? + { + // Since we can't delete account data events, we're going to treat + // deserialization failures as secret storage being disabled. + Ok(content.deserialize_as::().is_ok()) + } else { + // No account data event found, must be disabled. + Ok(false) + } + } +} diff --git a/crates/matrix-sdk/src/encryption/secret_storage/secret_store.rs b/crates/matrix-sdk/src/encryption/secret_storage/secret_store.rs new file mode 100644 index 000000000..ab5fc61ab --- /dev/null +++ b/crates/matrix-sdk/src/encryption/secret_storage/secret_store.rs @@ -0,0 +1,421 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt; + +use matrix_sdk_base::crypto::{secret_storage::SecretStorageKey, CrossSigningKeyExport}; +use ruma::{ + events::{ + secret::request::SecretName, secret_storage::secret::SecretEventContent, + GlobalAccountDataEventType, + }, + serde::Raw, +}; +use serde_json::value::to_raw_value; +use tracing::{ + error, + field::{debug, display}, + info, instrument, Span, +}; + +use super::{DecryptionError, Result}; +use crate::Client; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// Secure key/value storage for Matrix users. +/// +/// The `SecretStore` struct encapsulates the secret storage mechanism for +/// Matrix users, as it is specified in the [Matrix specification]. +/// +/// This specialized storage is tied to the user's Matrix account and serves as +/// an encrypted key/value store, backed by [account data] residing on the +/// homeserver. Any secrets uploaded to the homeserver using the +/// [`SecretStore::put_secret()`] method are automatically encrypted by the +/// [`SecretStore`]. +/// +/// [`SecretStore`] enables you to safely manage and access sensitive +/// information while ensuring that it remains protected from unauthorized +/// access. It plays a crucial role in maintaining the privacy and security of a +/// Matrix user's data. +/// +/// **Data Flow Overview:** +/// ```mermaid +/// flowchart LR +/// subgraph Client +/// SecretStore +/// end +/// subgraph Homeserver +/// data[Account Data] +/// end +/// SecretStore <== Encrypted ==> data +/// ``` +/// +/// **Note**: It's important to emphasize that the `SecretStore` should not be +/// used for storing large volumes of data due to its nature as a key/value +/// store for sensitive information. +/// +/// # Examples +/// +/// ```no_run +/// # use matrix_sdk::Client; +/// # use url::Url; +/// # async { +/// # let homeserver = Url::parse("http://example.com")?; +/// # let client = Client::new(homeserver).await?; +/// use ruma::events::secret::request::SecretName; +/// +/// let secret_store = client +/// .encryption() +/// .secret_storage() +/// .open_secret_store("It's a secret to everybody") +/// .await?; +/// +/// let my_secret = "Top secret secret"; +/// let my_secret_name = SecretName::from("m.treasure"); +/// +/// secret_store.put_secret(my_secret_name, my_secret); +/// +/// # anyhow::Ok(()) }; +/// ``` +/// +/// [Matrix specification]: https://spec.matrix.org/v1.8/client-server-api/#secret-storage +/// [account data]: https://spec.matrix.org/v1.8/client-server-api/#client-config +pub struct SecretStore { + pub(super) client: Client, + pub(super) key: SecretStorageKey, +} + +impl SecretStore { + /// Export the [`SecretStorageKey`] of this [`SecretStore`] as a + /// base58-encoded string as defined in the [spec]. + /// + /// *Note*: This returns a copy of the private key material of the + /// [`SecretStorageKey`] as a string. The caller needs to ensure that this + /// string is zeroized. + /// + /// [spec]: https://spec.matrix.org/v1.8/client-server-api/#key-representation + pub fn secret_storage_key(&self) -> String { + self.key.to_base58() + } + + /// Retrieve a secret from the homeserver's account data + /// + /// This method allows you to retrieve a secret from the account data stored + /// on the Matrix homeserver. + /// + /// # Arguments + /// + /// - `secret_name`: The name of the secret. The provided `secret_name` + /// serves as the event type for the associated account data event. + /// + /// The `retrieve_secret` method enables you to access and decrypt secrets + /// previously stored in the user's account data on the homeserver. You can + /// use the `secret_name` parameter to specify the desired secret to + /// retrieve. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// use ruma::events::secret::request::SecretName; + /// + /// let secret_store = client + /// .encryption() + /// .secret_storage() + /// .open_secret_store("It's a secret to everybody") + /// .await?; + /// + /// let my_secret_name = SecretName::from("m.treasure"); + /// + /// let secret = secret_store.get_secret(my_secret_name).await?; + /// + /// # anyhow::Ok(()) }; + /// ``` + pub async fn get_secret(&self, secret_name: impl Into) -> Result> { + let secret_name = secret_name.into(); + let event_type = GlobalAccountDataEventType::from(secret_name.to_owned()); + + if let Some(secret_content) = self.client.account().fetch_account_data(event_type).await? { + let mut secret_content = secret_content.deserialize_as::()?; + + // The `SecretEventContent` contains a map from the secret storage key ID to the + // ciphertext. Let's try to find a secret which was encrypted using our + // [`SecretStorageKey`]. + if let Some(secret_content) = secret_content.encrypted.remove(self.key.key_id()) { + // We found a secret we should be able to decrypt, let's try to do so. + let decrypted = self + .key + .decrypt(&secret_content.try_into()?, &secret_name) + .map_err(DecryptionError::from)?; + + let secret = String::from_utf8(decrypted).map_err(DecryptionError::from)?; + + Ok(Some(secret)) + } else { + // We did not find a secret which was encrypted using our [`SecretStorageKey`], + // no need to try to decrypt. + Ok(None) + } + } else { + Ok(None) + } + } + + /// Store a secret in the homeserver's account data + /// + /// This method allows you to securely store a secret on the Matrix + /// homeserver as an encrypted account data event. + /// + /// # Arguments + /// + /// - `secret_name`: The name of the secret. The provided `secret_name` + /// serves as the event type for the account data event on the homeserver. + /// + /// - `secret`: The secret to be stored on the homeserver. The secret is + /// encrypted before being stored, ensuring its confidentiality and + /// integrity. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// use ruma::events::secret::request::SecretName; + /// + /// let secret_store = client + /// .encryption() + /// .secret_storage() + /// .open_secret_store("It's a secret to everybody") + /// .await?; + /// + /// let my_secret = "Top secret secret"; + /// let my_secret_name = SecretName::from("m.treasure"); + /// + /// secret_store.put_secret(my_secret_name, my_secret); + /// + /// # anyhow::Ok(()) }; + /// ``` + pub async fn put_secret(&self, secret_name: impl Into, secret: &str) -> Result<()> { + // This function does a read/update/store of an account data event stored on the + // homeserver. We first fetch the existing account data event, the event + // contains a map which gets updated by this method, finally we upload the + // modified event. + // + // To prevent multiple calls to this method trying to update a secret at the + // same time, and thus trampling on each other we introduce a lock which + // acts as a semaphore. + // + // Technically there's a low chance of this happening since we're not storing + // many secrets and the bigger problem is that another client might be + // doing this as well and the server doesn't have a mechanism to protect against + // this. + // + // We could make this lock be per `secret_name` but this is not a performance + // critical method. + let _guard = self.client.locks().store_secret_lock.lock().await; + + let secret_name = secret_name.into(); + let event_type = GlobalAccountDataEventType::from(secret_name.to_owned()); + + // Get the existing account data event or create a new empty one. + let mut secret_content = if let Some(secret_content) = + self.client.account().fetch_account_data(event_type.to_owned()).await? + { + secret_content + .deserialize_as::() + .unwrap_or_else(|_| SecretEventContent::new(Default::default())) + } else { + SecretEventContent::new(Default::default()) + }; + + // Encrypt the secret. + let secret = secret.as_bytes().to_vec(); + let encrypted_secret = self.key.encrypt(secret, &secret_name); + + // Insert the encrypted secret into the account data event. + secret_content.encrypted.insert(self.key.key_id().to_owned(), encrypted_secret.into()); + let secret_content = Raw::from_json(to_raw_value(&secret_content)?); + + // Upload the modified account data event, now that the new secret has been + // inserted. + self.client.account().set_account_data_raw(event_type, secret_content).await?; + + Ok(()) + } + + /// Get all the well-known private parts/keys of the [`OwnUserIdentity`] as + /// a [`CrossSigningKeyExport`]. + /// + /// The export can be imported into the [`OlmMachine`] using + /// [`OlmMachine::import_cross_signing_keys()`]. + async fn get_cross_signing_keys(&self) -> Result { + let mut export = CrossSigningKeyExport::default(); + + export.master_key = self.get_secret(SecretName::CrossSigningMasterKey).await?; + export.self_signing_key = self.get_secret(SecretName::CrossSigningSelfSigningKey).await?; + export.user_signing_key = self.get_secret(SecretName::CrossSigningUserSigningKey).await?; + + Ok(export) + } + + async fn put_cross_signing_keys(&self, export: CrossSigningKeyExport) -> Result<()> { + if let Some(master_key) = &export.master_key { + self.put_secret(SecretName::CrossSigningMasterKey, master_key).await?; + } + + if let Some(user_signing_key) = &export.user_signing_key { + self.put_secret(SecretName::CrossSigningUserSigningKey, user_signing_key).await?; + } + + if let Some(self_signing_key) = &export.self_signing_key { + self.put_secret(SecretName::CrossSigningSelfSigningKey, self_signing_key).await?; + } + + Ok(()) + } + + /// Retrieve and store well-known secrets locally + /// + /// This method retrieves and stores all well-known secrets from the account + /// data on the Matrix homeserver to enhance local security and identity + /// verification. + /// + /// The following secrets are retrieved by this method: + /// + /// - `m.cross_signing.master`: The master cross-signing key. + /// - `m.cross_signing.self_signing`: The self-signing cross-signing key. + /// - `m.cross_signing.user_signing`: The user-signing cross-signing key. + /// + /// If the `m.cross_signing.self_signing` key is successfully imported, it + /// is used to sign our own [`Device`], marking it as verified. This step is + /// establishes trust in your own device's identity. + /// + /// By invoking this method, you ensure that your device has access to + /// the necessary secrets for device and identity verification. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::Client; + /// # use url::Url; + /// # async { + /// # let homeserver = Url::parse("http://example.com")?; + /// # let client = Client::new(homeserver).await?; + /// use ruma::events::secret::request::SecretName; + /// + /// let secret_store = client + /// .encryption() + /// .secret_storage() + /// .open_secret_store("It's a secret to everybody") + /// .await?; + /// + /// secret_store.import_secrets().await?; + /// + /// let status = client + /// .encryption() + /// .cross_signing_status() + /// .await + /// .expect("We should be able to check out cross-signing status"); + /// + /// println!("Cross-signing status {status:?}"); + /// + /// # anyhow::Ok(()) }; + /// ``` + /// + /// [`Device`]: crate::encryption::identities::Device + #[instrument(fields(user_id, device_id, cross_signing_status))] + pub async fn import_secrets(&self) -> Result<()> { + let olm_machine = self.client.olm_machine().await; + let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?; + + Span::current() + .record("user_id", display(olm_machine.user_id())) + .record("device_id", display(olm_machine.device_id())); + + info!("Fetching the private cross-signing keys from the secret store"); + + // Get all our private cross-signing keys from the secret store. + let export = self.get_cross_signing_keys().await?; + + info!(cross_signing_keys = ?export, "Received the cross signing keys from the server"); + + // We need to ensure that we have the public parts of the cross-signing keys, + // those are represented as the `OwnUserIdentity` struct. The public + // parts from the server are compared to the public parts re-derived from the + // private parts. We will only import the private parts of the cross-signing + // keys if they match to the public parts, otherwise we would risk + // importing some stale cross-signing keys leftover in the secret store. + let (request_id, request) = olm_machine.query_keys_for_users([olm_machine.user_id()]); + self.client.keys_query(&request_id, request.device_keys).await?; + + // TODO: Import the backup key here as well if it exists and enable backups if + // the current backup version is trusted, or expose a different method for this? + + // Let's now try to import our private cross-signing keys. + let status = olm_machine.import_cross_signing_keys(export).await?; + + Span::current().record("cross_signing_status", debug(&status)); + + info!("Done importing the cross signing keys"); + + if status.has_self_signing { + info!("Successfully imported the self-signing key, attempting to sign our own device"); + + // Now that we successfully imported them, the self-signing key can be used to + // verify our own device so other devices and user identities trust + // it if the trust our user identity. + if let Some(own_device) = self.client.encryption().get_own_device().await? { + own_device.verify().await?; + + // Another /keys/query request to ensure that the signatures we uploaded using + // `own_device.verify()` are attached to the `Device` we have in storage. + let (request_id, request) = + olm_machine.query_keys_for_users([olm_machine.user_id()]); + self.client.keys_query(&request_id, request.device_keys).await?; + + info!("Successfully signed our own device, the device is now verified"); + } else { + error!("Couldn't find our own device in the store"); + } + } + + Ok(()) + } + + pub(super) async fn export_secrets(&self) -> Result<()> { + let olm_machine = self.client.olm_machine().await; + let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?; + + if let Some(cross_signing_keys) = olm_machine.export_cross_signing_keys().await? { + self.put_cross_signing_keys(cross_signing_keys).await?; + } + + // TODO: export the backup key as well. + + Ok(()) + } +} + +impl fmt::Debug for SecretStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SecretStore").field("key", &self.key).finish_non_exhaustive() + } +}