// Copyright 2020 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. //! Types and traits to implement the storage layer for the [`OlmMachine`] //! //! The storage layer for the [`OlmMachine`] can be customized using a trait. //! Implementing your own [`CryptoStore`] //! //! An in-memory only store is provided as well as a Sled based one, depending //! on your needs and targets a custom store may be implemented, e.g. for //! `wasm-unknown-unknown` an indexeddb store would be needed //! //! ``` //! # use matrix_sdk_crypto::{ //! # OlmMachine, //! # store::MemoryStore, //! # }; //! # use ruma::{user_id, DeviceIdBox}; //! # let user_id = user_id!("@example:localhost"); //! # let device_id: DeviceIdBox = "TEST".into(); //! let store = Box::new(MemoryStore::new()); //! //! let machine = OlmMachine::new_with_store(user_id, device_id, store); //! ``` //! //! [`OlmMachine`]: /matrix_sdk_crypto/struct.OlmMachine.html //! [`CryptoStore`]: trait.Cryptostore.html pub mod caches; mod memorystore; mod pickle_key; #[cfg(feature = "sled_cryptostore")] pub(crate) mod sled; use std::{ collections::{HashMap, HashSet}, fmt::Debug, io::Error as IoError, ops::Deref, sync::Arc, }; use base64::DecodeError; use matrix_sdk_common::{async_trait, locks::Mutex, uuid::Uuid, AsyncTraitDeps}; pub use memorystore::MemoryStore; use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError}; pub use pickle_key::{EncryptedPickleKey, PickleKey}; use ruma::{ events::secret::request::SecretName, identifiers::Error as IdentifierValidationError, DeviceId, DeviceIdBox, DeviceKeyAlgorithm, RoomId, UserId, }; use serde_json::Error as SerdeError; use thiserror::Error; use tracing::{info, warn}; use zeroize::Zeroize; #[cfg(feature = "sled_cryptostore")] pub use self::sled::SledStore; use crate::{ error::SessionUnpicklingError, gossiping::{GossipRequest, SecretInfo}, identities::{ user::{OwnUserIdentity, UserIdentities, UserIdentity}, Device, ReadOnlyDevice, ReadOnlyUserIdentities, UserDevices, }, olm::{ InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount, Session, }, verification::VerificationMachine, CrossSigningStatus, }; /// A `CryptoStore` specific result type. pub type Result = std::result::Result; /// A wrapper for our CryptoStore trait object. /// /// This is needed because we want to have a generic interface so we can /// store/restore objects that we can serialize. Since trait objects and /// generics don't mix let the CryptoStore store strings and this wrapper /// adds the generic interface on top. #[derive(Debug, Clone)] pub(crate) struct Store { user_id: Arc, identity: Arc>, inner: Arc, verification_machine: VerificationMachine, } #[derive(Clone, Debug, Default)] #[allow(missing_docs)] pub struct Changes { pub account: Option, pub private_identity: Option, pub sessions: Vec, pub message_hashes: Vec, pub inbound_group_sessions: Vec, pub outbound_group_sessions: Vec, pub identities: IdentityChanges, pub key_requests: Vec, pub devices: DeviceChanges, } #[derive(Debug, Clone, Default)] #[allow(missing_docs)] pub struct IdentityChanges { pub new: Vec, pub changed: Vec, } #[derive(Debug, Clone, Default)] #[allow(missing_docs)] pub struct DeviceChanges { pub new: Vec, pub changed: Vec, pub deleted: Vec, } impl DeviceChanges { /// Merge the given `DeviceChanges` into this instance of `DeviceChanges`. pub fn extend(&mut self, other: DeviceChanges) { self.new.extend(other.new); self.changed.extend(other.changed); self.deleted.extend(other.deleted); } } /// A struct containing private cross signing keys that can be backed up or /// uploaded to the secret store. #[derive(Zeroize)] #[zeroize(drop)] pub struct CrossSigningKeyExport { /// The seed of the master key encoded as unpadded base64. pub master_key: Option, /// The seed of the self signing key encoded as unpadded base64. pub self_signing_key: Option, /// The seed of the user signing key encoded as unpadded base64. pub user_signing_key: Option, } impl Debug for CrossSigningKeyExport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CrossSigningKeyExport") .field("master_key", &self.master_key.is_some()) .field("self_signing_key", &self.self_signing_key.is_some()) .field("user_signing_key", &self.user_signing_key.is_some()) .finish_non_exhaustive() } } /// Error describing what went wrong when importing private cross signing keys /// or the key backup key. #[derive(Debug, Error)] pub enum SecretImportError { /// The seed for the private key wasn't valid base64. #[error(transparent)] Base64(#[from] DecodeError), /// The public key of the imported private key doesn't match to the public /// key that was uploaded to the server. #[error( "The public key of the imported private key doesn't match to the \ public key that was uploaded to the server" )] MissmatchedPublicKeys, /// The new version of the identity couldn't be stored. #[error(transparent)] Store(#[from] CryptoStoreError), } impl Store { pub fn new( user_id: Arc, identity: Arc>, store: Arc, verification_machine: VerificationMachine, ) -> Self { Self { user_id, identity, inner: store, verification_machine } } pub fn user_id(&self) -> &UserId { &self.user_id } pub fn device_id(&self) -> &DeviceId { self.verification_machine.own_device_id() } #[cfg(test)] pub async fn reset_cross_signing_identity(&self) { self.identity.lock().await.reset().await; } pub fn private_identity(&self) -> Arc> { self.identity.clone() } pub async fn save_sessions(&self, sessions: &[Session]) -> Result<()> { let changes = Changes { sessions: sessions.to_vec(), ..Default::default() }; self.save_changes(changes).await } #[cfg(test)] pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> { let changes = Changes { devices: DeviceChanges { changed: devices.to_vec(), ..Default::default() }, ..Default::default() }; self.save_changes(changes).await } #[cfg(test)] pub async fn save_inbound_group_sessions( &self, sessions: &[InboundGroupSession], ) -> Result<()> { let changes = Changes { inbound_group_sessions: sessions.to_vec(), ..Default::default() }; self.save_changes(changes).await } /// Get the display name of our own device. pub async fn device_display_name(&self) -> Result, CryptoStoreError> { Ok(self .inner .get_device(self.user_id(), self.device_id()) .await? .and_then(|d| d.display_name().to_owned())) } /// Get the read-only version of all the devices that the given user has. /// /// *Note*: This doesn't return our own device. pub async fn get_readonly_device( &self, user_id: &UserId, device_id: &DeviceId, ) -> Result> { if user_id == self.user_id() && device_id == self.device_id() { Ok(None) } else { self.inner.get_device(user_id, device_id).await } } /// Get the read-only version of all the devices that the given user has. /// /// *Note*: This doesn't return our own device. pub async fn get_readonly_devices( &self, user_id: &UserId, ) -> Result> { self.inner.get_user_devices(user_id).await.map(|mut d| { if user_id == self.user_id() { d.remove(self.device_id()); } d }) } /// Get the read-only version of all the devices that the given user has. /// /// *Note*: This does also return our own device. pub async fn get_readonly_devices_unfiltered( &self, user_id: &UserId, ) -> Result> { self.inner.get_user_devices(user_id).await } /// Get a device for the given user with the given curve25519 key. /// /// *Note*: This doesn't return our own device. pub async fn get_device_from_curve_key( &self, user_id: &UserId, curve_key: &str, ) -> Result> { self.get_user_devices(user_id).await.map(|d| { d.devices().find(|d| { d.get_key(DeviceKeyAlgorithm::Curve25519).map_or(false, |k| k == curve_key) }) }) } pub async fn get_user_devices(&self, user_id: &UserId) -> Result { let mut devices = self.inner.get_user_devices(user_id).await?; if user_id == self.user_id() { devices.remove(self.device_id()); } let own_identity = self.inner.get_user_identity(&self.user_id).await?.map(|i| i.own().cloned()).flatten(); let device_owner_identity = self.inner.get_user_identity(user_id).await.ok().flatten(); Ok(UserDevices { inner: devices, verification_machine: self.verification_machine.clone(), own_identity, device_owner_identity, }) } pub async fn get_device( &self, user_id: &UserId, device_id: &DeviceId, ) -> Result> { if user_id == self.user_id() && device_id == self.device_id() { Ok(None) } else { let own_identity = self .inner .get_user_identity(&self.user_id) .await? .map(|i| i.own().cloned()) .flatten(); let device_owner_identity = self.inner.get_user_identity(user_id).await?; Ok(self.inner.get_device(user_id, device_id).await?.map(|d| Device { inner: d, verification_machine: self.verification_machine.clone(), own_identity, device_owner_identity, })) } } pub async fn get_identity(&self, user_id: &UserId) -> Result> { // let own_identity = // self.inner.get_user_identity(self.user_id()).await?.and_then(|i| i.own()); Ok(if let Some(identity) = self.inner.get_user_identity(user_id).await? { Some(match identity { ReadOnlyUserIdentities::Own(i) => OwnUserIdentity { inner: i, verification_machine: self.verification_machine.clone(), } .into(), ReadOnlyUserIdentities::Other(i) => { let own_identity = self.inner.get_user_identity(self.user_id()).await?.and_then(|i| { if let ReadOnlyUserIdentities::Own(i) = i { Some(i) } else { None } }); UserIdentity { inner: i, verification_machine: self.verification_machine.clone(), own_identity, } .into() } }) } else { None }) } /// Try to export the secret with the given secret name. /// /// The exported secret will be encoded as unpadded base64. Returns `Null` /// if the secret can't be found. /// /// # Arguments /// /// * `secret_name` - The name of the secret that should be exported. pub async fn export_secret(&self, secret_name: &SecretName) -> Option { match secret_name { SecretName::CrossSigningMasterKey | SecretName::CrossSigningUserSigningKey | SecretName::CrossSigningSelfSigningKey => { self.identity.lock().await.export_secret(secret_name).await } SecretName::RecoveryKey => None, name => { warn!(secret =? name, "Unknown secret was requested"); None } } } pub async fn import_cross_signing_keys( &self, export: CrossSigningKeyExport, ) -> Result { if let Some(public_identity) = self.get_identity(&self.user_id).await?.and_then(|i| i.own()) { let identity = self.identity.lock().await; identity .import_secrets( public_identity, export.master_key.as_deref(), export.self_signing_key.as_deref(), export.user_signing_key.as_deref(), ) .await?; let status = identity.status().await; info!(status =? status, "Successfully imported the private cross signing keys"); let changes = Changes { private_identity: Some(identity.clone()), ..Default::default() }; self.save_changes(changes).await?; } Ok(self.identity.lock().await.status().await) } pub async fn import_secret( &self, secret_name: &SecretName, secret: String, ) -> Result<(), SecretImportError> { let secret = zeroize::Zeroizing::new(secret); match secret_name { SecretName::CrossSigningMasterKey | SecretName::CrossSigningUserSigningKey | SecretName::CrossSigningSelfSigningKey => { if let Some(public_identity) = self.get_identity(&self.user_id).await?.and_then(|i| i.own()) { let identity = self.identity.lock().await; identity.import_secret(public_identity, secret_name, &secret).await?; info!( secret_name = secret_name.as_ref(), "Successfully imported a private cross signing key" ); let changes = Changes { private_identity: Some(identity.clone()), ..Default::default() }; self.save_changes(changes).await?; } } SecretName::RecoveryKey => (), name => { warn!(secret =? name, "Tried to import an unknown secret"); } } Ok(()) } } impl Deref for Store { type Target = dyn CryptoStore; fn deref(&self) -> &Self::Target { &*self.inner } } #[derive(Debug, Error)] /// The crypto store's error type. pub enum CryptoStoreError { /// The account that owns the sessions, group sessions, and devices wasn't /// found. #[error("can't save/load sessions or group sessions in the store before an account is stored")] AccountUnset, /// Error in the internal database #[cfg(feature = "sled_cryptostore")] #[error(transparent)] Database(#[from] sled::Error), /// An IO error occurred. #[error(transparent)] Io(#[from] IoError), /// The underlying Olm Account operation returned an error. #[error(transparent)] OlmAccount(#[from] OlmAccountError), /// The underlying Olm session operation returned an error. #[error(transparent)] OlmSession(#[from] OlmSessionError), /// The underlying Olm group session operation returned an error. #[error(transparent)] OlmGroupSession(#[from] OlmGroupSessionError), /// A session time-stamp couldn't be loaded. #[error(transparent)] SessionUnpickling(#[from] SessionUnpicklingError), /// Failed to decrypt an pickled object. #[error("An object failed to be decrypted while unpickling")] UnpicklingError, /// A Matrix identifier failed to be validated. #[error(transparent)] IdentifierValidation(#[from] IdentifierValidationError), /// The store failed to (de)serialize a data type. #[error(transparent)] Serialization(#[from] SerdeError), } /// Trait abstracting a store that the `OlmMachine` uses to store cryptographic /// keys. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait CryptoStore: AsyncTraitDeps { /// Load an account that was previously stored. async fn load_account(&self) -> Result>; /// Save the given account in the store. /// /// # Arguments /// /// * `account` - The account that should be stored. async fn save_account(&self, account: ReadOnlyAccount) -> Result<()>; /// Try to load a private cross signing identity, if one is stored. async fn load_identity(&self) -> Result>; /// Save the set of changes to the store. /// /// # Arguments /// /// * `changes` - The set of changes that should be stored. async fn save_changes(&self, changes: Changes) -> Result<()>; /// Get all the sessions that belong to the given sender key. /// /// # Arguments /// /// * `sender_key` - The sender key that was used to establish the sessions. async fn get_sessions(&self, sender_key: &str) -> Result>>>>; /// Get the inbound group session from our store. /// /// # Arguments /// * `room_id` - The room id of the room that the session belongs to. /// /// * `sender_key` - The sender key that sent us the session. /// /// * `session_id` - The unique id of the session. async fn get_inbound_group_session( &self, room_id: &RoomId, sender_key: &str, session_id: &str, ) -> Result>; /// Get all the inbound group sessions we have stored. async fn get_inbound_group_sessions(&self) -> Result>; /// Get the outbound group sessions we have stored that is used for the /// given room. async fn get_outbound_group_sessions( &self, room_id: &RoomId, ) -> Result>; /// Is the given user already tracked. fn is_user_tracked(&self, user_id: &UserId) -> bool; /// Are there any tracked users that are marked as dirty. fn has_users_for_key_query(&self) -> bool; /// Set of users that we need to query keys for. This is a subset of /// the tracked users. fn users_for_key_query(&self) -> HashSet; /// Get all tracked users we know about. fn tracked_users(&self) -> HashSet; /// Add an user for tracking. /// /// Returns true if the user wasn't already tracked, false otherwise. /// /// # Arguments /// /// * `user` - The user that should be marked as tracked. /// /// * `dirty` - Should the user be also marked for a key query. async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result; /// Get the device for the given user with the given device id. /// /// # Arguments /// /// * `user_id` - The user that the device belongs to. /// /// * `device_id` - The unique id of the device. async fn get_device( &self, user_id: &UserId, device_id: &DeviceId, ) -> Result>; /// Get all the devices of the given user. /// /// # Arguments /// /// * `user_id` - The user for which we should get all the devices. async fn get_user_devices( &self, user_id: &UserId, ) -> Result>; /// Get the user identity that is attached to the given user id. /// /// # Arguments /// /// * `user_id` - The user for which we should get the identity. async fn get_user_identity(&self, user_id: &UserId) -> Result>; /// Check if a hash for an Olm message stored in the database. async fn is_message_known(&self, message_hash: &OlmMessageHash) -> Result; /// Get an outgoing secret request that we created that matches the given /// request id. /// /// # Arguments /// /// * `request_id` - The unique request id that identifies this outgoing /// secret request. async fn get_outgoing_secret_requests(&self, request_id: Uuid) -> Result>; /// Get an outgoing key request that we created that matches the given /// requested key info. /// /// # Arguments /// /// * `key_info` - The key info of an outgoing secret request. async fn get_secret_request_by_info( &self, secret_info: &SecretInfo, ) -> Result>; /// Get all outgoing secret requests that we have in the store. async fn get_unsent_secret_requests(&self) -> Result>; /// Delete an outgoing key request that we created that matches the given /// request id. /// /// # Arguments /// /// * `request_id` - The unique request id that identifies this outgoing key /// request. async fn delete_outgoing_secret_requests(&self, request_id: Uuid) -> Result<()>; }