From 2b39476d9b2f49b0ebedf4c08665ea7a3127aefa Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 13 Dec 2024 13:03:43 +0100 Subject: [PATCH] feat(crypto): Support storing the dehydrated device pickle key --- .../src/dehydrated_devices.rs | 17 +-- crates/matrix-sdk-crypto/CHANGELOG.md | 7 ++ .../src/dehydrated_devices.rs | 116 +++++++++++++++--- .../src/store/integration_tests.rs | 49 +++++++- .../src/store/memorystore.rs | 36 +++++- crates/matrix-sdk-crypto/src/store/mod.rs | 47 +++++++ crates/matrix-sdk-crypto/src/store/traits.rs | 19 ++- .../src/crypto_store/mod.rs | 37 +++++- crates/matrix-sdk-sqlite/src/crypto_store.rs | 28 ++++- crates/matrix-sdk-sqlite/src/utils.rs | 18 +++ 10 files changed, 342 insertions(+), 32 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs index 585eb7a9b..ae05014c9 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs @@ -1,8 +1,11 @@ use std::{mem::ManuallyDrop, sync::Arc}; -use matrix_sdk_crypto::dehydrated_devices::{ - DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices, - RehydratedDevice as InnerRehydratedDevice, +use matrix_sdk_crypto::{ + dehydrated_devices::{ + DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices, + RehydratedDevice as InnerRehydratedDevice, + }, + store::DehydratedDeviceKey, }; use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId}; use serde_json::json; @@ -177,13 +180,13 @@ impl From } } -fn get_pickle_key(pickle_key: &[u8]) -> Result, DehydrationError> { +fn get_pickle_key(pickle_key: &[u8]) -> Result { let pickle_key_length = pickle_key.len(); if pickle_key_length == 32 { - let mut key = Box::new([0u8; 32]); - key.copy_from_slice(pickle_key); - + let mut raw_bytes = [0u8; 32]; + raw_bytes.copy_from_slice(pickle_key); + let key = DehydratedDeviceKey::from_bytes(&raw_bytes); Ok(key) } else { Err(DehydrationError::PickleKeyLength(pickle_key_length)) diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 32716cf91..9a22e9f8d 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +- Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key` + and `DehydratedDevices::delete_dehydrated_device_pickle_key` to store/load the dehydrated device pickle key. + This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and to_device accumulation. + [**breaking**] `DehydratedDevices::keys_for_upload` and `DehydratedDevices::rehydrate` now use the `DehydratedDeviceKey` + as parameter instead of a raw byte array. Use `DehydratedDeviceKey::from_bytes` to migrate. + ([#4383](https://github.com/matrix-org/matrix-rust-sdk/pull/4383)) + - Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`. These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors when the sender either did not wish to share or was unable to share the room_key. diff --git a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs index f10a180fc..5231e1f44 100644 --- a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs +++ b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs @@ -57,7 +57,7 @@ use tracing::{instrument, trace}; use vodozemac::LibolmPickleError; use crate::{ - store::{CryptoStoreWrapper, MemoryStore, RoomKeyInfo, Store}, + store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store}, verification::VerificationMachine, Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError, }; @@ -132,15 +132,49 @@ impl DehydratedDevices { /// private keys of the device. pub async fn rehydrate( &self, - pickle_key: &[u8; 32], + pickle_key: &DehydratedDeviceKey, device_id: &DeviceId, device_data: Raw, ) -> Result { - let pickle_key = expand_pickle_key(pickle_key, device_id); + let pickle_key = expand_pickle_key(pickle_key.inner.as_ref(), device_id); let rehydrated = self.inner.rehydrate(&pickle_key, device_id, device_data).await?; Ok(RehydratedDevice { rehydrated, original: self.inner.to_owned() }) } + + /// Get the cached dehydrated device pickle key if any. + /// + /// None if the key was not previously cached (via + /// [`DehydratedDevices::save_dehydrated_device_pickle_key`]). + /// + /// Should be used to periodically rotate the dehydrated device to avoid + /// one-time keys exhaustion and accumulation of to_device messages. + pub async fn get_dehydrated_device_pickle_key( + &self, + ) -> Result, DehydrationError> { + Ok(self.inner.store().load_dehydrated_device_pickle_key().await?) + } + + /// Store the dehydrated device pickle key in the crypto store. + /// + /// This is useful if the client wants to periodically rotate dehydrated + /// devices to avoid one-time keys exhaustion and accumulated to_device + /// problems. + pub async fn save_dehydrated_device_pickle_key( + &self, + dehydrated_device_pickle_key: &DehydratedDeviceKey, + ) -> Result<(), DehydrationError> { + let changes = Changes { + dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key.clone()), + ..Default::default() + }; + Ok(self.inner.store().save_changes(changes).await?) + } + + /// Deletes the previously stored dehydrated device pickle key. + pub async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), DehydrationError> { + Ok(self.inner.store().delete_dehydrated_device_pickle_key().await?) + } } /// A rehydraded device. @@ -170,7 +204,7 @@ impl RehydratedDevice { /// /// ```no_run /// # use anyhow::Result; - /// # use matrix_sdk_crypto::OlmMachine; + /// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey }; /// # use ruma::{api::client::dehydrated_device, DeviceId}; /// # async fn example() -> Result<()> { /// # let machine: OlmMachine = unimplemented!(); @@ -184,9 +218,9 @@ impl RehydratedDevice { /// ) -> Result { /// todo!("Download the to-device events of the dehydrated device"); /// } - /// - /// // Don't use a zero key for real. - /// let pickle_key = [0u8; 32]; + /// // Get the cached dehydrated key (got it after verification/recovery) + /// let pickle_key = machine + /// .dehydrated_devices().get_dehydrated_device_pickle_key().await?.unwrap(); /// /// // Fetch the dehydrated device from the server. /// let response = get_dehydrated_device().await?; @@ -285,11 +319,13 @@ impl DehydratedDevice { /// # Examples /// /// ```no_run - /// # use matrix_sdk_crypto::OlmMachine; - /// # async fn example() -> anyhow::Result<()> { + /// # use matrix_sdk_crypto::OlmMachine; /// # + /// use matrix_sdk_crypto::store::DehydratedDeviceKey; + /// + /// async fn example() -> anyhow::Result<()> { /// # let machine: OlmMachine = unimplemented!(); - /// // Don't use a zero key for real. - /// let pickle_key = [0u8; 32]; + /// // Create a new random key + /// let pickle_key = DehydratedDeviceKey::new()?; /// /// // Create the dehydrated device. /// let device = machine.dehydrated_devices().create().await?; @@ -299,6 +335,9 @@ impl DehydratedDevice { /// .keys_for_upload("Dehydrated device".to_owned(), &pickle_key) /// .await?; /// + /// // Save the key if you want to later one rotate the dehydrated device + /// machine.dehydrated_devices().save_dehydrated_device_pickle_key(&pickle_key).await.unwrap(); + /// /// // Send the request out using your HTTP client. /// // client.send(request).await?; /// # Ok(()) @@ -314,7 +353,7 @@ impl DehydratedDevice { pub async fn keys_for_upload( &self, initial_device_display_name: String, - pickle_key: &[u8; 32], + pickle_key: &DehydratedDeviceKey, ) -> Result { let mut transaction = self.store.transaction().await; @@ -330,7 +369,8 @@ impl DehydratedDevice { trace!("Creating an upload request for a dehydrated device"); - let pickle_key = expand_pickle_key(pickle_key, &self.store.static_account().device_id); + let pickle_key = + expand_pickle_key(pickle_key.inner.as_ref(), &self.store.static_account().device_id); let device_id = self.store.static_account().device_id.clone(); let device_data = account.dehydrate(&pickle_key); let initial_device_display_name = Some(initial_device_display_name); @@ -393,12 +433,15 @@ mod tests { tests::to_device_requests_to_content, }, olm::OutboundGroupSession, + store::DehydratedDeviceKey, types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType}, utilities::json_convert, EncryptionSettings, OlmMachine, }; - const PICKLE_KEY: &[u8; 32] = &[0u8; 32]; + fn pickle_key() -> DehydratedDeviceKey { + DehydratedDeviceKey::from_bytes(&[0u8; 32]) + } fn user_id() -> &'static UserId { user_id!("@alice:localhost") @@ -467,7 +510,7 @@ mod tests { let dehydrated_device = olm_machine.dehydrated_devices().create().await.unwrap(); let request = dehydrated_device - .keys_for_upload("Foo".to_owned(), PICKLE_KEY) + .keys_for_upload("Foo".to_owned(), &pickle_key()) .await .expect("We should be able to create a request to upload a dehydrated device"); @@ -497,7 +540,7 @@ mod tests { let dehydrated_device = alice.dehydrated_devices().create().await.unwrap(); let mut request = dehydrated_device - .keys_for_upload("Foo".to_owned(), PICKLE_KEY) + .keys_for_upload("Foo".to_owned(), &pickle_key()) .await .expect("We should be able to create a request to upload a dehydrated device"); @@ -531,7 +574,7 @@ mod tests { // Rehydrate the device. let rehydrated = bob .dehydrated_devices() - .rehydrate(PICKLE_KEY, &request.device_id, request.device_data) + .rehydrate(&pickle_key(), &request.device_id, request.device_data) .await .expect("We should be able to rehydrate the device"); @@ -561,4 +604,43 @@ mod tests { "The session ids of the imported room key and the outbound group session should match" ); } + + #[async_test] + async fn test_dehydrated_device_pickle_key_cache() { + let alice = get_olm_machine().await; + + let dehydrated_manager = alice.dehydrated_devices(); + + let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap(); + assert!(stored_key.is_none()); + + let pickle_key = DehydratedDeviceKey::new().unwrap(); + + dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap(); + + let stored_key = + dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap().unwrap(); + assert_eq!(stored_key.to_base64(), pickle_key.to_base64()); + + let dehydrated_device = dehydrated_manager.create().await.unwrap(); + + let request = dehydrated_device + .keys_for_upload("Foo".to_owned(), &stored_key) + .await + .expect("We should be able to create a request to upload a dehydrated device"); + + // Rehydrate the device. + dehydrated_manager + .rehydrate(&stored_key, &request.device_id, request.device_data) + .await + .expect("We should be able to rehydrate the device"); + + dehydrated_manager + .delete_dehydrated_device_pickle_key() + .await + .expect("Should be able to delete the dehydrated device key"); + + let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap(); + assert!(stored_key.is_none()); + } } diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index a84442f45..410d8dbde 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -51,7 +51,7 @@ macro_rules! cryptostore_integration_tests { PrivateCrossSigningIdentity, SenderData, SenderDataType, Session }, store::{ - BackupDecryptionKey, Changes, CryptoStore, DeviceChanges, GossipRequest, + BackupDecryptionKey, Changes, CryptoStore, DehydratedDeviceKey, DeviceChanges, GossipRequest, IdentityChanges, PendingChanges, RoomSettings, }, testing::{get_device, get_other_identity, get_own_identity}, @@ -1217,6 +1217,53 @@ macro_rules! cryptostore_integration_tests { assert!(restored.backup_version.is_some(), "The backup version should now be Some as well"); } + #[async_test] + async fn test_dehydration_pickle_key_saving() { + let (_account, store) = get_loaded_store("dehydration_key_saving").await; + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_none(), "Initially no pickle key should be present"); + + let dehydrated_device_pickle_key = Some(DehydratedDeviceKey::new().unwrap()); + let exported_base64 = dehydrated_device_pickle_key.clone().unwrap().to_base64(); + + let changes = Changes { dehydrated_device_pickle_key, ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_some(), "We should be able to restore a pickle key"); + assert_eq!(restored.unwrap().to_base64(), exported_base64); + + // If None, should not clear the existing saved key + let changes = Changes { dehydrated_device_pickle_key: None, ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_some(), "We should be able to restore a pickle key"); + assert_eq!(restored.unwrap().to_base64(), exported_base64); + + } + + #[async_test] + async fn test_delete_dehydration_pickle_key() { + let (_account, store) = get_loaded_store("dehydration_key_saving").await; + + let dehydrated_device_pickle_key = DehydratedDeviceKey::new().unwrap(); + + let changes = Changes { dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key), ..Default::default() }; + store.save_changes(changes).await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_some(), "We should be able to restore a pickle key"); + + store.delete_dehydrated_device_pickle_key().await.unwrap(); + + let restored = store.load_dehydrated_device_pickle_key().await.unwrap(); + assert!(restored.is_none(), "The previously saved key should be deleted"); + + } + + #[async_test] async fn test_custom_value_saving() { let (_, store) = get_loaded_store("custom_value_saving").await; diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 8d5da3503..90557acf6 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -31,8 +31,8 @@ use vodozemac::Curve25519PublicKey; use super::{ caches::{DeviceStore, GroupSessionStore}, - Account, BackupKeys, Changes, CryptoStore, InboundGroupSession, PendingChanges, RoomKeyCounts, - RoomSettings, Session, + Account, BackupKeys, Changes, CryptoStore, DehydratedDeviceKey, InboundGroupSession, + PendingChanges, RoomKeyCounts, RoomSettings, Session, }; use crate::{ gossiping::{GossipRequest, GossippedSecret, SecretInfo}, @@ -93,6 +93,7 @@ pub struct MemoryStore { leases: StdRwLock>, secret_inbox: StdRwLock>>, backup_keys: RwLock, + dehydrated_device_pickle_key: RwLock>, next_batch_token: RwLock>, room_settings: StdRwLock>, } @@ -116,6 +117,7 @@ impl Default for MemoryStore { custom_values: Default::default(), leases: Default::default(), backup_keys: Default::default(), + dehydrated_device_pickle_key: Default::default(), secret_inbox: Default::default(), next_batch_token: Default::default(), room_settings: Default::default(), @@ -268,6 +270,11 @@ impl CryptoStore for MemoryStore { self.backup_keys.write().await.backup_version = Some(version); } + if let Some(pickle_key) = changes.dehydrated_device_pickle_key { + let mut lock = self.dehydrated_device_pickle_key.write().await; + *lock = Some(pickle_key); + } + { let mut secret_inbox = self.secret_inbox.write().unwrap(); for secret in changes.secrets { @@ -486,6 +493,16 @@ impl CryptoStore for MemoryStore { Ok(self.backup_keys.read().await.to_owned()) } + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + Ok(self.dehydrated_device_pickle_key.read().await.to_owned()) + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> { + let mut lock = self.dehydrated_device_pickle_key.write().await; + *lock = None; + Ok(()) + } + async fn get_outbound_group_session( &self, room_id: &RoomId, @@ -1125,7 +1142,10 @@ mod integration_tests { InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, StaticAccountData, }, - store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings}, + store::{ + BackupKeys, Changes, CryptoStore, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, + RoomSettings, + }, types::events::room_key_withheld::RoomKeyWithheldEvent, Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, Session, TrackedUser, UserIdentityData, @@ -1288,6 +1308,16 @@ mod integration_tests { self.0.load_backup_keys().await } + async fn load_dehydrated_device_pickle_key( + &self, + ) -> Result, Self::Error> { + self.0.load_dehydrated_device_pickle_key().await + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error> { + self.0.delete_dehydrated_device_pickle_key().await + } + async fn get_outbound_group_session( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 61427b138..0fa133824 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -518,6 +518,7 @@ pub struct Changes { pub private_identity: Option, pub backup_version: Option, pub backup_decryption_key: Option, + pub dehydrated_device_pickle_key: Option, pub sessions: Vec, pub message_hashes: Vec, pub inbound_group_sessions: Vec, @@ -550,6 +551,7 @@ impl Changes { self.private_identity.is_none() && self.backup_version.is_none() && self.backup_decryption_key.is_none() + && self.dehydrated_device_pickle_key.is_none() && self.sessions.is_empty() && self.message_hashes.is_empty() && self.inbound_group_sessions.is_empty() @@ -749,6 +751,51 @@ impl Debug for BackupDecryptionKey { } } +/// The pickle key used to safely store the dehydrated device pickle. +/// +/// This input key material will be expanded using HKDF into an AES key, MAC +/// key, and an initialization vector (IV). +#[derive(Clone, Zeroize, ZeroizeOnDrop, Deserialize, Serialize)] +#[serde(transparent)] +pub struct DehydratedDeviceKey { + pub(crate) inner: Box<[u8; DehydratedDeviceKey::KEY_SIZE]>, +} + +impl DehydratedDeviceKey { + /// The number of bytes the encryption key will hold. + pub const KEY_SIZE: usize = 32; + + /// Generates a new random pickle key. + pub fn new() -> Result { + let mut rng = rand::thread_rng(); + + let mut key = Box::new([0u8; Self::KEY_SIZE]); + rand::Fill::try_fill(key.as_mut_slice(), &mut rng)?; + + Ok(Self { inner: key }) + } + + /// Creates a dehydration pickle key from the given bytes. + pub fn from_bytes(raw_key: &[u8; 32]) -> Self { + let mut inner = Box::new([0u8; Self::KEY_SIZE]); + inner.copy_from_slice(raw_key); + + Self { inner } + } + + /// Export the [`DehydratedDeviceKey`] as a base64 encoded string. + pub fn to_base64(&self) -> String { + base64_encode(self.inner.as_slice()) + } +} + +#[cfg(not(tarpaulin_include))] +impl Debug for DehydratedDeviceKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("DehydratedDeviceKey").field(&"...").finish() + } +} + impl DeviceChanges { /// Merge the given `DeviceChanges` into this instance of `DeviceChanges`. pub fn extend(&mut self, other: DeviceChanges) { diff --git a/crates/matrix-sdk-crypto/src/store/traits.rs b/crates/matrix-sdk-crypto/src/store/traits.rs index 3031e33ef..3e651061d 100644 --- a/crates/matrix-sdk-crypto/src/store/traits.rs +++ b/crates/matrix-sdk-crypto/src/store/traits.rs @@ -22,7 +22,8 @@ use ruma::{ use vodozemac::Curve25519PublicKey; use super::{ - BackupKeys, Changes, CryptoStoreError, PendingChanges, Result, RoomKeyCounts, RoomSettings, + BackupKeys, Changes, CryptoStoreError, DehydratedDeviceKey, PendingChanges, Result, + RoomKeyCounts, RoomSettings, }; #[cfg(doc)] use crate::olm::SenderData; @@ -195,6 +196,14 @@ pub trait CryptoStore: AsyncTraitDeps { /// Get the backup keys we have stored. async fn load_backup_keys(&self) -> Result; + /// Get the dehydrated device pickle key we have stored. + async fn load_dehydrated_device_pickle_key( + &self, + ) -> Result, Self::Error>; + + /// Deletes the previously stored dehydrated device pickle key. + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error>; + /// Get the outbound group session we have stored that is used for the /// given room. async fn get_outbound_group_session( @@ -465,6 +474,14 @@ impl CryptoStore for EraseCryptoStoreError { self.0.load_backup_keys().await.map_err(Into::into) } + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + self.0.load_dehydrated_device_pickle_key().await.map_err(Into::into) + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error> { + self.0.delete_dehydrated_device_pickle_key().await.map_err(Into::into) + } + async fn get_outbound_group_session( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 5f4c05532..94136c6b6 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -29,8 +29,8 @@ use matrix_sdk_crypto::{ StaticAccountData, }, store::{ - BackupKeys, Changes, CryptoStore, CryptoStoreError, PendingChanges, RoomKeyCounts, - RoomSettings, + BackupKeys, Changes, CryptoStore, CryptoStoreError, DehydratedDeviceKey, PendingChanges, + RoomKeyCounts, RoomSettings, }, types::events::room_key_withheld::RoomKeyWithheldEvent, vodozemac::base64_encode, @@ -104,6 +104,9 @@ mod keys { /// with the client-side recovery key, which is actually an AES key for use /// with SSSS. pub const RECOVERY_KEY_V1: &str = "recovery_key_v1"; + + /// Indexeddb key for the dehydrated device pickle key. + pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key"; } /// An implementation of [CryptoStore] that uses [IndexedDB] for persistent @@ -471,6 +474,7 @@ impl IndexeddbCryptoStore { let decryption_key_pickle = &changes.backup_decryption_key; let backup_version = &changes.backup_version; + let dehydration_pickle_key = &changes.dehydrated_device_pickle_key; let mut core = indexeddb_changes.get(keys::CORE); if let Some(next_batch) = &changes.next_batch_token { @@ -487,6 +491,13 @@ impl IndexeddbCryptoStore { ); } + if let Some(i) = &dehydration_pickle_key { + core.put( + JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY), + self.serializer.serialize_value(i)?, + ); + } + if let Some(a) = &decryption_key_pickle { indexeddb_changes.get(keys::BACKUP_KEYS).put( JsValue::from_str(keys::RECOVERY_KEY_V1), @@ -1291,6 +1302,28 @@ impl_crypto_store! { Ok(key) } + + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + if let Some(pickle) = self + .inner + .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)? + .object_store(keys::CORE)? + .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))? + .await? + { + let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?; + + Ok(Some(pickle)) + } else { + Ok(None) + } + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> { + self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?; + Ok(()) + } + async fn get_withheld_info( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index b16a05e18..aa4e3e9d2 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -27,7 +27,10 @@ use matrix_sdk_crypto::{ InboundGroupSession, OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session, StaticAccountData, }, - store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings}, + store::{ + BackupKeys, Changes, CryptoStore, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, + RoomSettings, + }, types::events::room_key_withheld::RoomKeyWithheldEvent, Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, }; @@ -189,6 +192,9 @@ impl SqliteCryptoStore { const DATABASE_VERSION: u8 = 9; +/// key for the dehydrated device pickle key in the key/value table. +const DEHYDRATED_DEVICE_PICKLE_KEY: &str = "dehydrated_device_pickle_key"; + /// Run migrations for the given version of the database. async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { if version == 0 { @@ -846,6 +852,11 @@ impl CryptoStore for SqliteCryptoStore { txn.set_kv("backup_version_v1", &serialized_backup_version)?; } + if let Some(pickle_key) = &changes.dehydrated_device_pickle_key { + let serialized_pickle_key = this.serialize_value(pickle_key)?; + txn.set_kv(DEHYDRATED_DEVICE_PICKLE_KEY, &serialized_pickle_key)?; + } + for device in changes.devices.new.iter().chain(&changes.devices.changed) { let user_id = this.encode_key("device", device.user_id().as_bytes()); let device_id = this.encode_key("device", device.device_id().as_bytes()); @@ -1091,6 +1102,21 @@ impl CryptoStore for SqliteCryptoStore { Ok(BackupKeys { backup_version, decryption_key }) } + async fn load_dehydrated_device_pickle_key(&self) -> Result> { + let conn = self.acquire().await?; + + conn.get_kv(DEHYDRATED_DEVICE_PICKLE_KEY) + .await? + .map(|value| self.deserialize_value(&value)) + .transpose() + } + + async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), Self::Error> { + let conn = self.acquire().await?; + conn.clear_kv(DEHYDRATED_DEVICE_PICKLE_KEY).await?; + + Ok(()) + } async fn get_outbound_group_session( &self, room_id: &RoomId, diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index 04f549f8f..7daa9358b 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -257,6 +257,9 @@ pub(crate) trait SqliteKeyValueStoreConnExt { /// Store the given value for the given key. fn set_kv(&self, key: &str, value: &[u8]) -> rusqlite::Result<()>; + /// Removes the current key and value if exists. + fn clear_kv(&self, key: &str) -> rusqlite::Result<()>; + /// Set the version of the database. fn set_db_version(&self, version: u8) -> rusqlite::Result<()> { self.set_kv("version", &[version]) @@ -271,6 +274,11 @@ impl SqliteKeyValueStoreConnExt for rusqlite::Connection { )?; Ok(()) } + + fn clear_kv(&self, key: &str) -> rusqlite::Result<()> { + self.execute("DELETE FROM kv WHERE key = ?1", (key,))?; + Ok(()) + } } /// Extension trait for an [`SqliteAsyncConn`] that contains a key-value @@ -307,6 +315,9 @@ pub(crate) trait SqliteKeyValueStoreAsyncConnExt: SqliteAsyncConnExt { /// Store the given value for the given key. async fn set_kv(&self, key: &str, value: Vec) -> rusqlite::Result<()>; + /// Clears the given value for the given key. + async fn clear_kv(&self, key: &str) -> rusqlite::Result<()>; + /// Get the version of the database. async fn db_version(&self) -> Result { let kv_exists = self.kv_table_exists().await.map_err(OpenStoreError::LoadVersion)?; @@ -353,6 +364,13 @@ impl SqliteKeyValueStoreAsyncConnExt for SqliteAsyncConn { Ok(()) } + + async fn clear_kv(&self, key: &str) -> rusqlite::Result<()> { + let key = key.to_owned(); + self.interact(move |conn| conn.clear_kv(&key)).await.unwrap()?; + + Ok(()) + } } /// Repeat `?` n times, where n is defined by `count`. `?` are comma-separated.