feat(crypto): Support storing the dehydrated device pickle key

This commit is contained in:
Valere
2024-12-13 13:03:43 +01:00
parent 6dcefe49c2
commit 2b39476d9b
10 changed files with 342 additions and 32 deletions

View File

@@ -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<dehydrated_device::put_dehydrated_device::unstable::Request>
}
}
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
fn get_pickle_key(pickle_key: &[u8]) -> Result<DehydratedDeviceKey, DehydrationError> {
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))

View File

@@ -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.

View File

@@ -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<DehydratedDeviceData>,
) -> Result<RehydratedDevice, DehydrationError> {
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<Option<DehydratedDeviceKey>, 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<dehydrated_device::get_events::unstable::Response> {
/// 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<put_dehydrated_device::unstable::Request, DehydrationError> {
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());
}
}

View File

@@ -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;

View File

@@ -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<HashMap<String, (String, Instant)>>,
secret_inbox: StdRwLock<HashMap<String, Vec<GossippedSecret>>>,
backup_keys: RwLock<BackupKeys>,
dehydrated_device_pickle_key: RwLock<Option<DehydratedDeviceKey>>,
next_batch_token: RwLock<Option<String>>,
room_settings: StdRwLock<HashMap<OwnedRoomId, RoomSettings>>,
}
@@ -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<Option<DehydratedDeviceKey>> {
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<Option<DehydratedDeviceKey>, 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,

View File

@@ -518,6 +518,7 @@ pub struct Changes {
pub private_identity: Option<PrivateCrossSigningIdentity>,
pub backup_version: Option<String>,
pub backup_decryption_key: Option<BackupDecryptionKey>,
pub dehydrated_device_pickle_key: Option<DehydratedDeviceKey>,
pub sessions: Vec<Session>,
pub message_hashes: Vec<OlmMessageHash>,
pub inbound_group_sessions: Vec<InboundGroupSession>,
@@ -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<Self, rand::Error> {
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) {

View File

@@ -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<BackupKeys, Self::Error>;
/// Get the dehydrated device pickle key we have stored.
async fn load_dehydrated_device_pickle_key(
&self,
) -> Result<Option<DehydratedDeviceKey>, 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<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
self.0.load_backup_keys().await.map_err(Into::into)
}
async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
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,

View File

@@ -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<Option<DehydratedDeviceKey>> {
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,

View File

@@ -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<Option<DehydratedDeviceKey>> {
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,

View File

@@ -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<u8>) -> 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<u8, OpenStoreError> {
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.