diff --git a/Cargo.lock b/Cargo.lock index c4eb656732c..c9ae8743fbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,6 +2753,7 @@ dependencies = [ "pbkdf2", "proptest", "rand 0.8.5", + "rmp-serde", "ruma", "serde", "serde_json", diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 3903289acb4..3f872ede930 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -32,7 +32,7 @@ use matrix_sdk_common::deserialized_responses::VerificationState; use matrix_sdk_crypto::{ backups::SignatureState, olm::{IdentityKeys, InboundGroupSession, Session}, - store::{Changes, CryptoStore}, + store::{Changes, CryptoStore, RoomSettings as RustRoomSettings}, types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey}, EncryptionSettings as RustEncryptionSettings, LocalTrust, }; @@ -75,6 +75,8 @@ pub struct MigrationData { cross_signing: CrossSigningKeyExport, /// The list of users that the Rust SDK should track. tracked_users: Vec, + /// Map of room settings + room_settings: HashMap, } /// Struct collecting data that is important to migrate sessions to the rust-sdk @@ -296,6 +298,12 @@ async fn migrate_data( processed_steps += 1; listener(processed_steps, total_steps); + let mut room_settings = HashMap::new(); + for (room_id, settings) in data.room_settings { + let room_id = RoomId::parse(room_id)?; + room_settings.insert(room_id, settings.into()); + } + let changes = Changes { account: Some(account), private_identity: Some(cross_signing), @@ -303,6 +311,7 @@ async fn migrate_data( inbound_group_sessions, recovery_key, backup_version: data.backup_version, + room_settings, ..Default::default() }; @@ -471,6 +480,45 @@ fn collect_sessions( Ok((sessions, inbound_group_sessions)) } +/// Migrate room settings, including room algorithm and whether to block +/// untrusted devices from legacy store to Sqlite store. +/// +/// Note that this method should only be used if a client has already migrated +/// account data via [migrate](#method.migrate) method, which did not include +/// room settings. For a brand new migration, the [migrate](#method.migrate) +/// method will take care of room settings automatically, if provided. +/// +/// # Arguments +/// +/// * `room_settings` - Map of room settings +/// +/// * `path` - The path where the Sqlite store should be created. +/// +/// * `passphrase` - The passphrase that should be used to encrypt the data at +/// rest in the Sqlite store. **Warning**, if no passphrase is given, the store +/// and all its data will remain unencrypted. +pub fn migrate_room_settings( + room_settings: HashMap, + path: &str, + passphrase: Option, +) -> anyhow::Result<()> { + let runtime = Runtime::new()?; + runtime.block_on(async move { + let store = SqliteCryptoStore::open(path, passphrase.as_deref()).await?; + + let mut rust_settings = HashMap::new(); + for (room_id, settings) in room_settings { + let room_id = RoomId::parse(room_id)?; + rust_settings.insert(room_id, settings.into()); + } + + let changes = Changes { room_settings: rust_settings, ..Default::default() }; + store.save_changes(changes).await?; + + Ok(()) + }) +} + /// Callback that will be passed over the FFI to report progress pub trait ProgressListener { /// The callback that should be called on the Rust side @@ -490,6 +538,7 @@ impl ProgressListener for T { } /// An encryption algorithm to be used to encrypt messages sent to a room. +#[derive(Debug, Deserialize, Serialize, PartialEq)] pub enum EventEncryptionAlgorithm { /// Olm version 1 using Curve25519, AES-256, and SHA-256. OlmV1Curve25519AesSha2, @@ -500,12 +549,22 @@ pub enum EventEncryptionAlgorithm { impl From for RustEventEncryptionAlgorithm { fn from(a: EventEncryptionAlgorithm) -> Self { match a { - EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => { - RustEventEncryptionAlgorithm::OlmV1Curve25519AesSha2 - } - EventEncryptionAlgorithm::MegolmV1AesSha2 => { - RustEventEncryptionAlgorithm::MegolmV1AesSha2 + EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2, + EventEncryptionAlgorithm::MegolmV1AesSha2 => Self::MegolmV1AesSha2, + } + } +} + +impl TryFrom for EventEncryptionAlgorithm { + type Error = serde_json::Error; + + fn try_from(value: RustEventEncryptionAlgorithm) -> Result { + match value { + RustEventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => { + Ok(Self::OlmV1Curve25519AesSha2) } + RustEventEncryptionAlgorithm::MegolmV1AesSha2 => Ok(Self::MegolmV1AesSha2), + _ => Err(serde::de::Error::custom(format!("Unsupported algorithm {value}"))), } } } @@ -540,10 +599,10 @@ pub enum HistoryVisibility { impl From for RustHistoryVisibility { fn from(h: HistoryVisibility) -> Self { match h { - HistoryVisibility::Invited => RustHistoryVisibility::Invited, - HistoryVisibility::Joined => RustHistoryVisibility::Joined, - HistoryVisibility::Shared => RustHistoryVisibility::Shared, - HistoryVisibility::WorldReadable => RustHistoryVisibility::Shared, + HistoryVisibility::Invited => Self::Invited, + HistoryVisibility::Joined => Self::Joined, + HistoryVisibility::Shared => Self::Shared, + HistoryVisibility::WorldReadable => Self::Shared, } } } @@ -709,6 +768,34 @@ impl From for CrossSigningStatus { } } +/// Room encryption settings which are modified by state events or user options +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub struct RoomSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EventEncryptionAlgorithm, + /// Should untrusted devices receive the room key, or should they be + /// excluded from the conversation. + pub only_allow_trusted_devices: bool, +} + +impl TryFrom for RoomSettings { + type Error = serde_json::Error; + + fn try_from(value: RustRoomSettings) -> Result { + let algorithm = value.algorithm.try_into()?; + Ok(Self { algorithm, only_allow_trusted_devices: value.only_allow_trusted_devices }) + } +} + +impl From for RustRoomSettings { + fn from(value: RoomSettings) -> Self { + Self { + algorithm: value.algorithm.into(), + only_allow_trusted_devices: value.only_allow_trusted_devices, + } + } +} + fn parse_user_id(user_id: &str) -> Result { ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e)) } @@ -725,7 +812,7 @@ mod uniffi_types { RequestVerificationResult, StartSasResult, Verification, VerificationRequest, }, BackupKeys, CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, EncryptionSettings, - RoomKeyCounts, + EventEncryptionAlgorithm, RoomKeyCounts, RoomSettings, }; } @@ -736,7 +823,7 @@ mod test { use tempfile::tempdir; use super::MigrationData; - use crate::{migrate, OlmMachine}; + use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings}; #[test] fn android_migration() -> Result<()> { @@ -819,7 +906,17 @@ mod test { "@this-is-me:matrix.org", "@Amandine:matrix.org", "@ganfra:matrix.org" - ] + ], + "room_settings": { + "!AZkqtjvtwPAuyNOXEt:matrix.org": { + "algorithm": "OlmV1Curve25519AesSha2", + "only_allow_trusted_devices": true + }, + "!CWLUCoEWXSFyTCOtfL:matrix.org": { + "algorithm": "MegolmV1AesSha2", + "only_allow_trusted_devices": false + }, + } }); let migration_data: MigrationData = serde_json::from_value(data)?; @@ -848,6 +945,27 @@ mod test { let backup_keys = machine.get_backup_keys()?; assert!(backup_keys.is_some()); + let settings1 = machine.get_room_settings("!AZkqtjvtwPAuyNOXEt:matrix.org".into())?; + assert_eq!( + Some(RoomSettings { + algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2, + only_allow_trusted_devices: true + }), + settings1 + ); + + let settings2 = machine.get_room_settings("!CWLUCoEWXSFyTCOtfL:matrix.org".into())?; + assert_eq!( + Some(RoomSettings { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + only_allow_trusted_devices: false + }), + settings2 + ); + + let settings3 = machine.get_room_settings("!XYZ:matrix.org".into())?; + assert!(settings3.is_none()); + Ok(()) } } diff --git a/bindings/matrix-sdk-crypto-ffi/src/logger.rs b/bindings/matrix-sdk-crypto-ffi/src/logger.rs index ce9ea068741..c2c99111953 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/logger.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/logger.rs @@ -44,9 +44,13 @@ pub struct LoggerWrapper { pub fn set_logger(logger: Box) { let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) }; - let filter = EnvFilter::from_default_env().add_directive( - "matrix_sdk_crypto=trace".parse().expect("Can't parse logging filter directive"), - ); + let filter = EnvFilter::from_default_env() + .add_directive( + "matrix_sdk_crypto=trace".parse().expect("Can't parse logging filter directive"), + ) + .add_directive( + "matrix_sdk_sqlite=debug".parse().expect("Can't parse logging filter directive"), + ); let _ = tracing_subscriber::fmt() .with_writer(logger) diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 814bf021a8f..bc218f42be3 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -16,7 +16,7 @@ use matrix_sdk_crypto::{ }, decrypt_room_key_export, encrypt_room_key_export, olm::ExportedRoomKey, - store::RecoveryKey, + store::{Changes, RecoveryKey}, LocalTrust, OlmMachine as InnerMachine, UserIdentities, }; use ruma::{ @@ -53,9 +53,9 @@ use crate::{ responses::{response_from_string, OwnedResponse}, BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport, CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings, - KeyImportError, KeysImportResult, MegolmV1BackupKey, ProgressListener, Request, RequestType, - RequestVerificationResult, RoomKeyCounts, Sas, SignatureUploadRequest, StartSasResult, - UserIdentity, Verification, VerificationRequest, + EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey, + ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings, + Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest, }; /// A high level state machine that handles E2EE for Matrix. @@ -563,6 +563,101 @@ impl OlmMachine { .map(|r| r.into())) } + /// Get the stored room settings, such as the encryption algorithm or + /// whether to encrypt only for trusted devices. + /// + /// These settings can be modified via + /// [set_room_algorithm()](#method.set_room_algorithm) and + /// [set_room_only_allow_trusted_devices()](#method. + /// set_room_only_allow_trusted_devices) methods. + pub fn get_room_settings( + &self, + room_id: String, + ) -> Result, CryptoStoreError> { + let room_id = RoomId::parse(room_id)?; + let settings = self + .runtime + .block_on(self.inner.store().get_room_settings(&room_id))? + .map(|v| v.try_into()) + .transpose()?; + Ok(settings) + } + + /// Set the room algorithm used for encrypting messages to one of the + /// available variants + pub fn set_room_algorithm( + &self, + room_id: String, + algorithm: EventEncryptionAlgorithm, + ) -> Result<(), CryptoStoreError> { + let room_id = RoomId::parse(room_id)?; + self.runtime.block_on(async move { + let mut settings = + self.inner.store().get_room_settings(&room_id).await?.unwrap_or_default(); + settings.algorithm = algorithm.into(); + self.inner + .store() + .save_changes(Changes { + room_settings: HashMap::from([(room_id, settings)]), + ..Default::default() + }) + .await?; + Ok(()) + }) + } + + /// Set flag whether this room should encrypt messages for untrusted + /// devices, or whether they should be excluded from the conversation. + /// + /// Note that per-room setting may be overridden by a global + /// [set_only_allow_trusted_devices()](#method. + /// set_only_allow_trusted_devices) method. + pub fn set_room_only_allow_trusted_devices( + &self, + room_id: String, + only_allow_trusted_devices: bool, + ) -> Result<(), CryptoStoreError> { + let room_id = RoomId::parse(room_id)?; + self.runtime.block_on(async move { + let mut settings = + self.inner.store().get_room_settings(&room_id).await?.unwrap_or_default(); + settings.only_allow_trusted_devices = only_allow_trusted_devices; + self.inner + .store() + .save_changes(Changes { + room_settings: HashMap::from([(room_id, settings)]), + ..Default::default() + }) + .await?; + Ok(()) + }) + } + + /// Check whether there is a global flag to only encrypt messages for + /// trusted devices or for everyone. + /// + /// Note that if the global flag is false, individual rooms may still be + /// encrypting only for trusted devices, depending on the per-room + /// `only_allow_trusted_devices` flag. + pub fn get_only_allow_trusted_devices(&self) -> Result { + let block = self.runtime.block_on(self.inner.store().get_only_allow_trusted_devices())?; + Ok(block) + } + + /// Set global flag whether to encrypt messages for untrusted devices, or + /// whether they should be excluded from the conversation. + /// + /// Note that if enabled, it will override any per-room settings. + pub fn set_only_allow_trusted_devices( + &self, + only_allow_trusted_devices: bool, + ) -> Result<(), CryptoStoreError> { + self.runtime.block_on( + self.inner.store().set_only_allow_trusted_devices(only_allow_trusted_devices), + )?; + Ok(()) + } + /// Share a room key with the given list of users for the given room. /// /// After the request was sent out and a successful response was received diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 62aa0ffbc30..9f5e5fc88ce 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -14,6 +14,12 @@ namespace matrix_sdk_crypto_ffi { string? passphrase, ProgressListener progress_listener ); + [Throws=MigrationError] + void migrate_room_settings( + record room_settings, + [ByRef] string path, + string? passphrase + ); }; [Error] @@ -429,6 +435,7 @@ dictionary MigrationData { sequence pickle_key; CrossSigningKeyExport cross_signing; sequence tracked_users; + record room_settings; }; dictionary SessionMigrationData { @@ -466,3 +473,8 @@ dictionary PickledInboundGroupSession { boolean imported; boolean backed_up; }; + +dictionary RoomSettings { + EventEncryptionAlgorithm algorithm; + boolean only_allow_trusted_devices; +}; diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 7fd824c12d5..95c8ba24195 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -51,7 +51,7 @@ describe(OlmMachine.name, () => { expect(databases).toHaveLength(2); expect(databases).toStrictEqual([ { name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 }, - { name: `${store_name}::matrix-sdk-crypto`, version: 1 }, + { name: `${store_name}::matrix-sdk-crypto`, version: 2 }, ]); // Creating a new Olm machine, with the stored state. @@ -142,7 +142,7 @@ describe(OlmMachine.name, () => { expect(databases).toHaveLength(2); expect(databases).toStrictEqual([ { name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 }, - { name: `${store_name}::matrix-sdk-crypto`, version: 1 }, + { name: `${store_name}::matrix-sdk-crypto`, version: 2 }, ]); // Let's force to close the `OlmMachine`. diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index f8e8d0d747d..c787ca3c475 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -47,6 +47,7 @@ pbkdf2 = { version = "0.11.0", default-features = false } rand = "0.8.5" ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc2677"] } serde = { workspace = true, features = ["derive", "rc"] } +rmp-serde = "1.1.1" serde_json = { workspace = true } sha2 = "0.10.2" thiserror = { workspace = true } diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index f1c8766e1d5..e43ab2a841d 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -296,6 +296,11 @@ impl OlmMachine { Ok(OlmMachine::new_helper(user_id, device_id, store, account, identity)) } + /// Get the crypto store associated with this `OlmMachine` instance. + pub fn store(&self) -> &Store { + &self.store + } + /// The unique user id that owns this `OlmMachine` instance. pub fn user_id(&self) -> &UserId { &self.user_id @@ -750,6 +755,9 @@ impl OlmMachine { /// used. /// /// `users` - The list of users that should receive the room key. + /// + /// `settings` - Encryption settings that affect when are room keys rotated + /// and who are they shared with pub async fn share_room_key( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index b80414ff204..bc700b08fab 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -17,10 +17,12 @@ macro_rules! cryptostore_integration_tests { }, store::{ Changes, CryptoStore, DeviceChanges, GossipRequest, IdentityChanges, - RecoveryKey, + RecoveryKey, RoomSettings, }, testing::{get_device, get_other_identity, get_own_identity}, - types::events::room_key_request::MegolmV1AesSha2Content, + types::{ + events::room_key_request::MegolmV1AesSha2Content, EventEncryptionAlgorithm, + }, ReadOnlyDevice, SecretInfo, TrackedUser, }; @@ -646,6 +648,56 @@ macro_rules! cryptostore_integration_tests { "The loaded version matches to the one we stored" ); } + + #[async_test] + async fn room_settings_saving() { + let (account, store) = get_loaded_store("room_settings_saving").await; + + let room_1 = room_id!("!test_1:localhost"); + let settings_1 = RoomSettings { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + only_allow_trusted_devices: true, + }; + + let room_2 = room_id!("!test_2:localhost"); + let settings_2 = RoomSettings { + algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2, + only_allow_trusted_devices: false, + }; + + let room_3 = room_id!("!test_3:localhost"); + + let changes = Changes { + room_settings: HashMap::from([ + (room_1.into(), settings_1.clone()), + (room_2.into(), settings_2.clone()), + ]), + ..Default::default() + }; + + store.save_changes(changes).await.unwrap(); + + let loaded_settings_1 = store.get_room_settings(room_1).await.unwrap(); + assert_eq!(Some(settings_1), loaded_settings_1); + + let loaded_settings_2 = store.get_room_settings(room_2).await.unwrap(); + assert_eq!(Some(settings_2), loaded_settings_2); + + let loaded_settings_3 = store.get_room_settings(room_3).await.unwrap(); + assert_eq!(None, loaded_settings_3); + } + + #[async_test] + async fn custom_value_saving() { + let (account, store) = get_loaded_store("custom_value_saving").await; + store.set_custom_value("A", "Hello".as_bytes().to_vec()).await.unwrap(); + + let loaded_1 = store.get_custom_value("A").await.unwrap(); + assert_eq!(Some("Hello".as_bytes().to_vec()), loaded_1); + + let loaded_2 = store.get_custom_value("B").await.unwrap(); + assert_eq!(None, loaded_2); + } } }; } diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 87b3e39f891..7d31264358e 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -20,10 +20,12 @@ use matrix_sdk_common::locks::Mutex; use ruma::{ DeviceId, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; +use tracing::warn; use super::{ caches::{DeviceStore, GroupSessionStore, SessionStore}, - BackupKeys, Changes, CryptoStore, InboundGroupSession, ReadOnlyAccount, RoomKeyCounts, Session, + BackupKeys, Changes, CryptoStore, InboundGroupSession, ReadOnlyAccount, RoomKeyCounts, + RoomSettings, Session, }; use crate::{ gossiping::{GossipRequest, SecretInfo}, @@ -270,6 +272,21 @@ impl CryptoStore for MemoryStore { async fn load_backup_keys(&self) -> Result { Ok(BackupKeys::default()) } + + async fn get_room_settings(&self, _room_id: &RoomId) -> Result> { + warn!("Method not implemented"); + Ok(None) + } + + async fn get_custom_value(&self, _key: &str) -> Result>> { + warn!("Method not implemented"); + Ok(None) + } + + async fn set_custom_value(&self, _key: &str, _value: Vec) -> Result<()> { + warn!("Method not implemented"); + Ok(()) + } } #[cfg(test)] diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 265730b3274..c780359a2a0 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -48,8 +48,10 @@ use std::{ use atomic::Ordering; use dashmap::DashSet; use matrix_sdk_common::locks::Mutex; -use ruma::{events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedUserId, UserId}; -use serde::{Deserialize, Serialize}; +use ruma::{ + events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedUserId, UserId, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use thiserror::Error; use tracing::{info, warn}; use vodozemac::{megolm::SessionOrdering, Curve25519PublicKey}; @@ -64,6 +66,7 @@ use crate::{ InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount, Session, }, + types::EventEncryptionAlgorithm, utilities::encode, verification::VerificationMachine, CrossSigningStatus, @@ -93,7 +96,7 @@ pub use crate::gossiping::{GossipRequest, SecretInfo}; /// 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 { +pub struct Store { user_id: Arc, identity: Arc>, inner: Arc, @@ -118,6 +121,7 @@ pub struct Changes { pub key_requests: Vec, pub identities: IdentityChanges, pub devices: DeviceChanges, + pub room_settings: HashMap, } /// A user for which we are tracking the list of devices. @@ -277,9 +281,28 @@ pub enum SecretImportError { Store(#[from] CryptoStoreError), } +/// Room encryption settings which are modified by state events or user options +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct RoomSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EventEncryptionAlgorithm, + /// Should untrusted devices receive the room key, or should they be + /// excluded from the conversation. + pub only_allow_trusted_devices: bool, +} + +impl Default for RoomSettings { + fn default() -> Self { + Self { + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + only_allow_trusted_devices: false, + } + } +} + impl Store { /// Create a new Store - pub fn new( + pub(crate) fn new( user_id: Arc, identity: Arc>, store: Arc, @@ -298,33 +321,33 @@ impl Store { } /// UserId associated with this store - pub fn user_id(&self) -> &UserId { + pub(crate) fn user_id(&self) -> &UserId { &self.user_id } /// DeviceId associated with this store - pub fn device_id(&self) -> &DeviceId { + pub(crate) fn device_id(&self) -> &DeviceId { self.verification_machine.own_device_id() } /// The Account associated with this store - pub fn account(&self) -> &ReadOnlyAccount { + pub(crate) fn account(&self) -> &ReadOnlyAccount { &self.verification_machine.store.account } #[cfg(test)] /// test helper to reset the cross signing identity - pub async fn reset_cross_signing_identity(&self) { + pub(crate) async fn reset_cross_signing_identity(&self) { self.identity.lock().await.reset().await; } /// PrivateCrossSigningIdentity associated with this store - pub fn private_identity(&self) -> Arc> { + pub(crate) fn private_identity(&self) -> Arc> { self.identity.clone() } /// Save the given Sessions to the store - pub async fn save_sessions(&self, sessions: &[Session]) -> Result<()> { + pub(crate) async fn save_sessions(&self, sessions: &[Session]) -> Result<()> { let changes = Changes { sessions: sessions.to_vec(), ..Default::default() }; self.save_changes(changes).await @@ -336,7 +359,7 @@ impl Store { /// This method returns `SessionOrdering::Better` if the given session is /// better than the one we already have or if we don't have such a /// session in the store. - pub async fn compare_group_session( + pub(crate) async fn compare_group_session( &self, session: &InboundGroupSession, ) -> Result { @@ -352,7 +375,7 @@ impl Store { #[cfg(test)] /// Testing helper to allow to save only a set of devices - pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> { + pub(crate) async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> { let changes = Changes { devices: DeviceChanges { changed: devices.to_vec(), ..Default::default() }, ..Default::default() @@ -363,7 +386,7 @@ impl Store { #[cfg(test)] /// Testing helper to allo to save only a set of InboundGroupSession - pub async fn save_inbound_group_sessions( + pub(crate) async fn save_inbound_group_sessions( &self, sessions: &[InboundGroupSession], ) -> Result<()> { @@ -373,7 +396,7 @@ impl Store { } /// Get the display name of our own device. - pub async fn device_display_name(&self) -> Result, CryptoStoreError> { + pub(crate) async fn device_display_name(&self) -> Result, CryptoStoreError> { Ok(self .inner .get_device(self.user_id(), self.device_id()) @@ -382,7 +405,7 @@ impl Store { } /// Get the read-only device associated with `device_id` for `user_id` - pub async fn get_readonly_device( + pub(crate) async fn get_readonly_device( &self, user_id: &UserId, device_id: &DeviceId, @@ -393,7 +416,7 @@ impl Store { /// 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_filtered( + pub(crate) async fn get_readonly_devices_filtered( &self, user_id: &UserId, ) -> Result> { @@ -408,7 +431,7 @@ impl Store { /// 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( + pub(crate) async fn get_readonly_devices_unfiltered( &self, user_id: &UserId, ) -> Result> { @@ -418,7 +441,7 @@ impl Store { /// 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( + pub(crate) async fn get_device_from_curve_key( &self, user_id: &UserId, curve_key: Curve25519PublicKey, @@ -431,7 +454,7 @@ impl Store { /// Get all devices associated with the given `user_id` /// /// *Note*: This doesn't return our own device. - pub async fn get_user_devices_filtered(&self, user_id: &UserId) -> Result { + pub(crate) async fn get_user_devices_filtered(&self, user_id: &UserId) -> Result { self.get_user_devices(user_id).await.map(|mut d| { if user_id == self.user_id() { d.inner.remove(self.device_id()); @@ -443,7 +466,7 @@ impl Store { /// Get all devices associated with the given `user_id` /// /// *Note*: This does also return our own device. - pub async fn get_user_devices(&self, user_id: &UserId) -> Result { + pub(crate) async fn get_user_devices(&self, user_id: &UserId) -> Result { let devices = self.get_readonly_devices_unfiltered(user_id).await?; let own_identity = @@ -459,7 +482,7 @@ impl Store { } /// Get a Device copy associated with `device_id` for `user_id` - pub async fn get_device( + pub(crate) async fn get_device( &self, user_id: &UserId, device_id: &DeviceId, @@ -477,7 +500,7 @@ impl Store { } /// Get the Identity of `user_id` - pub async fn get_identity(&self, user_id: &UserId) -> Result> { + pub(crate) 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? { @@ -517,7 +540,7 @@ impl Store { /// # Arguments /// /// * `secret_name` - The name of the secret that should be exported. - pub async fn export_secret(&self, secret_name: &SecretName) -> Option { + pub(crate) async fn export_secret(&self, secret_name: &SecretName) -> Option { match secret_name { SecretName::CrossSigningMasterKey | SecretName::CrossSigningUserSigningKey @@ -544,7 +567,7 @@ impl Store { } /// Import the Cross Signing Keys - pub async fn import_cross_signing_keys( + pub(crate) async fn import_cross_signing_keys( &self, export: CrossSigningKeyExport, ) -> Result { @@ -574,7 +597,7 @@ impl Store { } /// Import the given `secret` named `secret_name` into the keystore. - pub async fn import_secret( + pub(crate) async fn import_secret( &self, secret_name: &SecretName, secret: &str, @@ -619,7 +642,7 @@ impl Store { /// /// This means that the user will be considered for a `/keys/query` request /// next time [`Store::users_for_key_query()`] is called. - pub async fn mark_user_as_changed(&self, user: &UserId) -> Result<()> { + pub(crate) async fn mark_user_as_changed(&self, user: &UserId) -> Result<()> { self.users_for_key_query.lock().await.insert_user(user); self.tracked_users_cache.insert(user.to_owned()); @@ -630,7 +653,10 @@ impl Store { /// /// Any users not already on the list are flagged as awaiting a key query. /// Users that were already in the list are unaffected. - pub async fn update_tracked_users(&self, users: impl Iterator) -> Result<()> { + pub(crate) async fn update_tracked_users( + &self, + users: impl Iterator, + ) -> Result<()> { self.load_tracked_users().await?; let mut store_updates = Vec::new(); @@ -653,7 +679,7 @@ impl Store { /// from the `/sync` response. Any users *whose device lists we are /// tracking* are flagged as needing a key query. Users whose devices we /// are not tracking are ignored. - pub async fn mark_tracked_users_as_changed( + pub(crate) async fn mark_tracked_users_as_changed( &self, users: impl Iterator, ) -> Result<()> { @@ -677,7 +703,7 @@ impl Store { /// This is called after processing the response to a /keys/query request. /// Any users whose device lists we are tracking are removed from the /// list of those pending a /keys/query. - pub async fn mark_tracked_users_as_up_to_date( + pub(crate) async fn mark_tracked_users_as_up_to_date( &self, users: impl Iterator, sequence_number: SequenceNumber, @@ -739,18 +765,64 @@ impl Store { /// A pair `(users, sequence_number)`, where `users` is the list of users to /// be queried, and `sequence_number` is the current sequence number, /// which should be returned in `mark_tracked_users_as_up_to_date`. - pub async fn users_for_key_query(&self) -> Result<(HashSet, SequenceNumber)> { + pub(crate) async fn users_for_key_query( + &self, + ) -> Result<(HashSet, SequenceNumber)> { self.load_tracked_users().await?; Ok(self.users_for_key_query.lock().await.users_for_key_query()) } /// See the docs for [`crate::OlmMachine::tracked_users()`]. - pub async fn tracked_users(&self) -> Result> { + pub(crate) async fn tracked_users(&self) -> Result> { self.load_tracked_users().await?; Ok(self.tracked_users_cache.iter().map(|u| u.clone()).collect()) } + + /// Check whether there is a global flag to only encrypt messages for + /// trusted devices or for everyone. + pub async fn get_only_allow_trusted_devices(&self) -> Result { + let value = self.get_value("only_allow_trusted_devices").await?.unwrap_or_default(); + Ok(value) + } + + /// Set global flag whether to encrypt messages for untrusted devices, or + /// whether they should be excluded from the conversation. + pub async fn set_only_allow_trusted_devices( + &self, + block_untrusted_devices: bool, + ) -> Result<()> { + self.set_value("only_allow_trusted_devices", &block_untrusted_devices).await + } + + /// Get custom stored value associated with a key + pub async fn get_value(&self, key: &str) -> Result> { + let Some(value) = self.get_custom_value(key).await? else { + return Ok(None); + }; + let deserialized = self.deserialize_value(&value)?; + Ok(Some(deserialized)) + } + + /// Store custom value associated with a key + pub async fn set_value(&self, key: &str, value: &impl Serialize) -> Result<()> { + let serialized = self.serialize_value(value)?; + self.set_custom_value(key, serialized).await?; + Ok(()) + } + + fn serialize_value(&self, value: &impl Serialize) -> Result> { + let serialized = + rmp_serde::to_vec_named(value).map_err(|x| CryptoStoreError::Backend(x.into()))?; + Ok(serialized) + } + + fn deserialize_value(&self, value: &[u8]) -> Result { + let deserialized = + rmp_serde::from_slice(value).map_err(|e| CryptoStoreError::Backend(e.into()))?; + Ok(deserialized) + } } impl Deref for Store { diff --git a/crates/matrix-sdk-crypto/src/store/traits.rs b/crates/matrix-sdk-crypto/src/store/traits.rs index 690abe2179f..43d7021b2da 100644 --- a/crates/matrix-sdk-crypto/src/store/traits.rs +++ b/crates/matrix-sdk-crypto/src/store/traits.rs @@ -18,7 +18,7 @@ use async_trait::async_trait; use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps}; use ruma::{DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId}; -use super::{BackupKeys, Changes, CryptoStoreError, Result, RoomKeyCounts}; +use super::{BackupKeys, Changes, CryptoStoreError, Result, RoomKeyCounts, RoomSettings}; use crate::{ olm::{ InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, @@ -186,6 +186,33 @@ pub trait CryptoStore: AsyncTraitDeps { &self, request_id: &TransactionId, ) -> Result<(), Self::Error>; + + /// Get the room settings, such as the encryption algorithm or whether to + /// encrypt only for trusted devices. + /// + /// # Arguments + /// + /// * `room_id` - The room id of the room + async fn get_room_settings( + &self, + room_id: &RoomId, + ) -> Result, Self::Error>; + + /// Get arbitrary data from the store + /// + /// # Arguments + /// + /// * `key` - The key to fetch data for + async fn get_custom_value(&self, key: &str) -> Result>, Self::Error>; + + /// Put arbitrary data into the store + /// + /// # Arguments + /// + /// * `key` - The key to insert data into + /// + /// * `value` - The value to insert + async fn set_custom_value(&self, key: &str, value: Vec) -> Result<(), Self::Error>; } #[repr(transparent)] @@ -312,6 +339,18 @@ impl CryptoStore for EraseCryptoStoreError { async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> { self.0.delete_outgoing_secret_requests(request_id).await.map_err(Into::into) } + + async fn get_room_settings(&self, room_id: &RoomId) -> Result> { + self.0.get_room_settings(room_id).await.map_err(Into::into) + } + + async fn get_custom_value(&self, key: &str) -> Result>, Self::Error> { + self.0.get_custom_value(key).await.map_err(Into::into) + } + + async fn set_custom_value(&self, key: &str, value: Vec) -> Result<(), Self::Error> { + self.0.set_custom_value(key, value).await.map_err(Into::into) + } } /// A type-erased [`CryptoStore`]. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store.rs b/crates/matrix-sdk-indexeddb/src/crypto_store.rs index cbb60e18e44..17a2b60149d 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store.rs @@ -28,6 +28,7 @@ use matrix_sdk_crypto::{ }, store::{ caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, RoomKeyCounts, + RoomSettings, }, GossipRequest, ReadOnlyAccount, ReadOnlyDevice, ReadOnlyUserIdentities, SecretInfo, TrackedUser, @@ -59,6 +60,7 @@ mod keys { pub const UNSENT_SECRET_REQUESTS: &str = "unsent_secret_requests"; pub const SECRET_REQUESTS_BY_INFO: &str = "secret_requests_by_info"; pub const KEY_REQUEST: &str = "key_request"; + pub const ROOM_SETTINGS: &str = "room_settings"; // keys pub const STORE_CIPHER: &str = "store_cipher"; @@ -142,7 +144,7 @@ impl IndexeddbCryptoStore { let name = format!("{prefix:0}::matrix-sdk-crypto"); // Open my_db v1 - let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.1)?; + let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 2.0)?; db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { let old_version = evt.old_version(); @@ -178,6 +180,11 @@ impl IndexeddbCryptoStore { db.create_object_store(keys::INBOUND_GROUP_SESSIONS)?; } + if old_version < 2.0 { + let db = evt.db(); + db.create_object_store(keys::ROOM_SETTINGS)?; + } + Ok(()) })); @@ -363,9 +370,11 @@ impl_crypto_store! { !changes.identities.new.is_empty() || !changes.identities.changed.is_empty(), keys::IDENTITIES, ), + (!changes.inbound_group_sessions.is_empty(), keys::INBOUND_GROUP_SESSIONS), (!changes.outbound_group_sessions.is_empty(), keys::OUTBOUND_GROUP_SESSIONS), (!changes.message_hashes.is_empty(), keys::OLM_HASHES), + (!changes.room_settings.is_empty(), keys::ROOM_SETTINGS), ] .iter() .filter_map(|(id, key)| if *id { Some(*key) } else { None }) @@ -474,6 +483,7 @@ impl_crypto_store! { let identity_changes = changes.identities; let olm_hashes = changes.message_hashes; let key_requests = changes.key_requests; + let room_settings_changes = changes.room_settings; if !device_changes.new.is_empty() || !device_changes.changed.is_empty() { let device_store = tx.object_store(keys::DEVICES)?; @@ -538,6 +548,16 @@ impl_crypto_store! { } } + if !room_settings_changes.is_empty() { + let settings_store = tx.object_store(keys::ROOM_SETTINGS)?; + + for (room_id, settings) in &room_settings_changes { + let key = self.encode_key(keys::ROOM_SETTINGS, room_id); + let value = self.serialize_value(&settings)?; + settings_store.put_key_val(&key, &value)?; + } + } + tx.await.into_result()?; // all good, let's update our caches:indexeddb @@ -950,6 +970,38 @@ impl_crypto_store! { Ok(key) } + + async fn get_room_settings(&self, room_id: &RoomId) -> Result> { + let key = self.encode_key(keys::ROOM_SETTINGS, room_id); + Ok(self + .inner + .transaction_on_one_with_mode(keys::ROOM_SETTINGS, IdbTransactionMode::Readonly)? + .object_store(keys::ROOM_SETTINGS)? + .get(&key)? + .await? + .map(|v| self.deserialize_value(v)) + .transpose()?) + } + + async fn get_custom_value(&self, key: &str) -> Result>> { + Ok(self + .inner + .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)? + .object_store(keys::CORE)? + .get(&JsValue::from_str(key))? + .await? + .map(|v| self.deserialize_value(v)) + .transpose()?) + } + + async fn set_custom_value(&self, key: &str, value: Vec) -> Result<()> { + self + .inner + .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)? + .object_store(keys::CORE)? + .put_key_val(&JsValue::from_str(key), &self.serialize_value(&value)?)?; + Ok(()) + } } impl Drop for IndexeddbCryptoStore { diff --git a/crates/matrix-sdk-sled/src/crypto_store.rs b/crates/matrix-sdk-sled/src/crypto_store.rs index 4b654824eb1..01652fb597b 100644 --- a/crates/matrix-sdk-sled/src/crypto_store.rs +++ b/crates/matrix-sdk-sled/src/crypto_store.rs @@ -28,7 +28,7 @@ use matrix_sdk_crypto::{ }, store::{ caches::SessionStore, BackupKeys, Changes, CryptoStore, CryptoStoreError, Result, - RoomKeyCounts, + RoomKeyCounts, RoomSettings, }, types::{events::room_key_request::SupportedKeyInfo, EventEncryptionAlgorithm}, GossipRequest, ReadOnlyAccount, ReadOnlyDevice, ReadOnlyUserIdentities, SecretInfo, @@ -46,7 +46,7 @@ use tracing::debug; use super::OpenStoreError; use crate::encode_key::{EncodeKey, ENCODE_SEPARATOR}; -const DATABASE_VERSION: u8 = 6; +const DATABASE_VERSION: u8 = 7; // Table names that are used to derive a separate key for each tree. This ensure // that user ids encoded for different trees won't end up as the same byte @@ -58,6 +58,7 @@ const INBOUND_GROUP_TABLE_NAME: &str = "crypto-store-inbound-group-sessions"; const OUTBOUND_GROUP_TABLE_NAME: &str = "crypto-store-outbound-group-sessions"; const SECRET_REQUEST_BY_INFO_TABLE: &str = "crypto-store-secret-request-by-info"; const TRACKED_USERS_TABLE: &str = "crypto-store-secret-tracked-users"; +const ROOM_SETTINGS_TABLE: &str = "crypto-store-secret-room-settings"; impl EncodeKey for InboundGroupSession { fn encode(&self) -> Vec { @@ -185,6 +186,8 @@ pub struct SledCryptoStore { identities: Tree, tracked_users: Tree, + + room_settings: Tree, } impl std::fmt::Debug for SledCryptoStore { @@ -386,6 +389,8 @@ impl SledCryptoStore { let unsent_secret_requests = db.open_tree("unsent_secret_requests")?; let secret_requests_by_info = db.open_tree("secret_requests_by_info")?; + let room_settings = db.open_tree("room_settings")?; + let session_cache = SessionStore::new(); let database = Self { @@ -406,6 +411,7 @@ impl SledCryptoStore { tracked_users, olm_hashes, identities, + room_settings, }; database.upgrade().await?; @@ -499,6 +505,7 @@ impl SledCryptoStore { let olm_hashes = changes.message_hashes; let key_requests = changes.key_requests; let backup_version = changes.backup_version; + let room_settings_changes = changes.room_settings; let ret: Result<(), TransactionError> = ( &self.account, @@ -512,6 +519,7 @@ impl SledCryptoStore { &self.outgoing_secret_requests, &self.unsent_secret_requests, &self.secret_requests_by_info, + &self.room_settings, ) .transaction( |( @@ -526,6 +534,7 @@ impl SledCryptoStore { outgoing_secret_requests, unsent_secret_requests, secret_requests_by_info, + room_settings, )| { if let Some(a) = &account_pickle { account.insert( @@ -635,6 +644,15 @@ impl SledCryptoStore { } } + for (room_id, settings) in &room_settings_changes { + let key = self.encode_key(ROOM_SETTINGS_TABLE, room_id); + room_settings.insert( + key.as_slice(), + self.serialize_value(&settings) + .map_err(ConflictableTransactionError::Abort)?, + )?; + } + Ok(()) }, ); @@ -1010,6 +1028,25 @@ impl CryptoStore for SledCryptoStore { Ok(key) } + + async fn get_room_settings(&self, room_id: &RoomId) -> Result> { + let key = self.encode_key(ROOM_SETTINGS_TABLE, room_id); + self.room_settings + .get(key) + .map_err(CryptoStoreError::backend)? + .map(|p| self.deserialize_value(&p)) + .transpose() + } + + async fn get_custom_value(&self, key: &str) -> Result>> { + let value = self.inner.get(key).map_err(CryptoStoreError::backend)?.map(|v| v.to_vec()); + Ok(value) + } + + async fn set_custom_value(&self, key: &str, value: Vec) -> Result<()> { + self.inner.insert(key, value).map_err(CryptoStoreError::backend)?; + Ok(()) + } } #[cfg(test)] diff --git a/crates/matrix-sdk-sqlite/migrations/003_room_settings.sql b/crates/matrix-sdk-sqlite/migrations/003_room_settings.sql new file mode 100644 index 00000000000..91fcbae1fd4 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/003_room_settings.sql @@ -0,0 +1,4 @@ +CREATE TABLE room_settings( + "room_id" BLOB PRIMARY KEY NOT NULL, + "data" BLOB NOT NULL +); diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 2b5523c1899..eedf4f0132b 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -27,7 +27,7 @@ use matrix_sdk_crypto::{ IdentityKeys, InboundGroupSession, OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity, Session, }, - store::{caches::SessionStore, BackupKeys, Changes, CryptoStore, RoomKeyCounts}, + store::{caches::SessionStore, BackupKeys, Changes, CryptoStore, RoomKeyCounts, RoomSettings}, GossipRequest, ReadOnlyAccount, ReadOnlyDevice, ReadOnlyUserIdentities, SecretInfo, TrackedUser, }; @@ -172,7 +172,7 @@ impl SqliteCryptoStore { } } -const DATABASE_VERSION: u8 = 2; +const DATABASE_VERSION: u8 = 3; async fn run_migrations(conn: &SqliteConn) -> rusqlite::Result<()> { let kv_exists = conn @@ -221,6 +221,13 @@ async fn run_migrations(conn: &SqliteConn) -> rusqlite::Result<()> { .await?; } + if version < 3 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!("../migrations/003_room_settings.sql")) + }) + .await?; + } + conn.set_kv("version", vec![DATABASE_VERSION]).await?; Ok(()) @@ -257,6 +264,8 @@ trait SqliteConnectionExt { sent_out: bool, data: &[u8], ) -> rusqlite::Result<()>; + + fn set_room_settings(&self, room_id: &[u8], data: &[u8]) -> rusqlite::Result<()>; } impl SqliteConnectionExt for rusqlite::Connection { @@ -348,6 +357,16 @@ impl SqliteConnectionExt for rusqlite::Connection { )?; Ok(()) } + + fn set_room_settings(&self, room_id: &[u8], data: &[u8]) -> rusqlite::Result<()> { + self.execute( + "INSERT INTO room_settings (room_id, data) + VALUES (?1, ?2) + ON CONFLICT (room_id) DO UPDATE SET data = ?2", + (room_id, data), + )?; + Ok(()) + } } #[async_trait] @@ -515,6 +534,15 @@ trait SqliteObjectCryptoStoreExt: SqliteObjectExt { self.execute("DELETE FROM key_requests WHERE request_id = ?", (request_id,)).await?; Ok(()) } + + async fn get_room_settings(&self, room_id: Key) -> Result>> { + Ok(self + .query_row("SELECT data FROM room_settings WHERE room_id = ?", (room_id,), |row| { + row.get(0) + }) + .await + .optional()?) + } } #[async_trait] @@ -690,6 +718,12 @@ impl CryptoStore for SqliteCryptoStore { txn.set_key_request(&request_id, request.sent_out, &serialized_request)?; } + for (room_id, settings) in changes.room_settings { + let room_id = this.encode_key("room_settings", room_id.as_bytes()); + let value = this.serialize_value(&settings)?; + txn.set_room_settings(&room_id, &value)?; + } + Ok::<_, Error>(()) }) .await?; @@ -946,6 +980,43 @@ impl CryptoStore for SqliteCryptoStore { let request_id = self.encode_key("key_requests", request_id.as_bytes()); Ok(self.acquire().await?.delete_key_request(request_id).await?) } + + async fn get_room_settings(&self, room_id: &RoomId) -> Result> { + let room_id = self.encode_key("room_settings", room_id.as_bytes()); + let Some(value) = self.acquire().await?.get_room_settings(room_id).await? else { + return Ok(None); + }; + + let settings = self.deserialize_value(&value)?; + + return Ok(Some(settings)); + } + + async fn get_custom_value(&self, key: &str) -> Result>> { + let Some(serialized) = self.acquire().await?.get_kv(key).await? else { + return Ok(None); + }; + let value = if let Some(cipher) = &self.store_cipher { + let encrypted = rmp_serde::from_slice(&serialized)?; + cipher.decrypt_value_data(encrypted)? + } else { + serialized + }; + + Ok(Some(value)) + } + + async fn set_custom_value(&self, key: &str, value: Vec) -> Result<()> { + let serialized = if let Some(cipher) = &self.store_cipher { + let encrypted = cipher.encrypt_value_data(value)?; + rmp_serde::to_vec_named(&encrypted)? + } else { + value + }; + + self.acquire().await?.set_kv(key, serialized).await?; + Ok(()) + } } #[cfg(test)]