Add secret storage support to the main SDK crate

This commit is contained in:
Damir Jelić
2023-09-26 12:56:10 +02:00
parent 61eb9cea8c
commit 16dda959e5
8 changed files with 855 additions and 3 deletions
Generated
+37 -2
View File
@@ -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",
]
+4
View File
@@ -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`
+1
View File
@@ -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 }
+16 -1
View File
@@ -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")]
+7
View File
@@ -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
@@ -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<SecretStore>;
#[cfg(target_arch = "wasm32")]
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 'a>>;
#[cfg(not(target_arch = "wasm32"))]
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + 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)
})
}
}
@@ -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<T, E = SecretStorageError> = std::result::Result<T, E>;
/// 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<String>,
},
/// 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<SecretStore> {
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::<SecretStorageDefaultKeyEventContent>()?;
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<bool> {
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::<SecretStorageDefaultKeyEventContent>().is_ok())
} else {
// No account data event found, must be disabled.
Ok(false)
}
}
}
@@ -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<SecretName>) -> Result<Option<String>> {
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::<SecretEventContent>()?;
// 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<SecretName>, 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::<SecretEventContent>()
.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<CrossSigningKeyExport> {
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()
}
}