mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-05 22:47:02 -04:00
feat(crypto): Support storing the dehydrated device pickle key
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user