From 39b9531724162a8c9d5cbe82246cb57eb390743d Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 10 Dec 2024 10:23:52 +0100 Subject: [PATCH] feat(crypto-bindings): Save/Load dehydrated pickle key --- .../src/dehydrated_devices.rs | 78 ++++++++++++++----- bindings/matrix-sdk-crypto-ffi/src/error.rs | 7 +- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 40 +++++++++- .../src/dehydrated_devices.rs | 4 + crates/matrix-sdk-crypto/src/store/mod.rs | 30 ++++++- 5 files changed, 136 insertions(+), 23 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs index ae05014c943..8a50de15c39 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs @@ -5,12 +5,13 @@ use matrix_sdk_crypto::{ DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices, RehydratedDevice as InnerRehydratedDevice, }, - store::DehydratedDeviceKey, + store::DehydratedDeviceKey as InnerDehydratedDeviceKey, }; use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId}; use serde_json::json; use tokio::runtime::Handle; -use zeroize::Zeroize; + +use crate::{CryptoStoreError, DehydratedDeviceKey}; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] @@ -25,6 +26,8 @@ pub enum DehydrationError { Store(#[from] matrix_sdk_crypto::CryptoStoreError), #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")] PickleKeyLength(usize), + #[error(transparent)] + Rand(#[from] rand::Error), } impl From for DehydrationError { @@ -36,6 +39,9 @@ impl From for Dehydrati Self::MissingSigningKey(e) } matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e), + matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => { + Self::PickleKeyLength(l) + } } } } @@ -69,14 +75,14 @@ impl DehydratedDevices { pub fn rehydrate( &self, - pickle_key: Vec, + pickle_key: &DehydratedDeviceKey, device_id: String, device_data: String, ) -> Result, DehydrationError> { let device_data: Raw<_> = serde_json::from_str(&device_data)?; let device_id: OwnedDeviceId = device_id.into(); - let mut key = get_pickle_key(&pickle_key)?; + let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?; let ret = RehydratedDevice { runtime: self.runtime.to_owned(), @@ -88,10 +94,41 @@ impl DehydratedDevices { } .into(); - key.zeroize(); - Ok(ret) } + + /// Get the cached dehydrated device pickle key if any. + /// + /// None if the key was not previously cached (via + /// [`Self::save_dehydrated_device_pickle_key`]). + /// + /// Should be used to periodically rotate the dehydrated device to avoid + /// OTK exhaustion and accumulation of to_device messages. + pub fn get_dehydrated_device_key( + &self, + ) -> Result, CryptoStoreError> { + Ok(self + .runtime + .block_on(self.inner.get_dehydrated_device_pickle_key())? + .map(crate::DehydratedDeviceKey::from)) + } + + /// Store the dehydrated device pickle key in the crypto store. + /// + /// This is useful if the client wants to periodically rotate dehydrated + /// devices to avoid OTK exhaustion and accumulated to_device problems. + pub fn save_dehydrated_device_key( + &self, + pickle_key: &crate::DehydratedDeviceKey, + ) -> Result<(), CryptoStoreError> { + let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?; + Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?) + } + + /// Deletes the previously stored dehydrated device pickle key. + pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> { + Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?) + } } #[derive(uniffi::Object)] @@ -141,15 +178,13 @@ impl DehydratedDevice { pub fn keys_for_upload( &self, device_display_name: String, - pickle_key: Vec, + pickle_key: &DehydratedDeviceKey, ) -> Result { - let mut key = get_pickle_key(&pickle_key)?; + let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?; let request = self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?; - key.zeroize(); - Ok(request.into()) } } @@ -180,15 +215,20 @@ impl From } } -fn get_pickle_key(pickle_key: &[u8]) -> Result { - let pickle_key_length = pickle_key.len(); +#[cfg(test)] +mod tests { + use crate::DehydratedDeviceKey; + + #[test] + fn test_dehydrated_key() { + let result = DehydratedDeviceKey::new(); + assert!(result.is_ok()); + let dehydrated_device_key = result.unwrap(); + let base_64 = dehydrated_device_key.to_base64(); + let inner_bytes = dehydrated_device_key.inner; + + let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap(); - if pickle_key_length == 32 { - 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)) + assert_eq!(base_64, copy.to_base64()); } } diff --git a/bindings/matrix-sdk-crypto-ffi/src/error.rs b/bindings/matrix-sdk-crypto-ffi/src/error.rs index 116244b68cf..e84685fe4a6 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/error.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/error.rs @@ -1,8 +1,9 @@ #![allow(missing_docs)] use matrix_sdk_crypto::{ - store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError, - SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError, + store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError}, + KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError, + SignatureError as InnerSignatureError, }; use matrix_sdk_sqlite::OpenStoreError; use ruma::{IdParseError, OwnedUserId}; @@ -57,6 +58,8 @@ pub enum CryptoStoreError { InvalidUserId(String, IdParseError), #[error(transparent)] Identifier(#[from] IdParseError), + #[error(transparent)] + DehydrationError(#[from] InnerDehydrationError), } #[derive(Debug, thiserror::Error, uniffi::Error)] diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 125abd99ed3..e6c09013edd 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification}; use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode}; use matrix_sdk_crypto::{ olm::{IdentityKeys, InboundGroupSession, SenderData, Session}, - store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings}, + store::{ + Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges, + RoomSettings as RustRoomSettings, + }, types::{ DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey, }, @@ -62,6 +65,8 @@ pub use verification::{ }; use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; +use crate::dehydrated_devices::DehydrationError; + /// Struct collecting data that is important to migrate to the rust-sdk #[derive(Deserialize, Serialize, uniffi::Record)] pub struct MigrationData { @@ -822,6 +827,39 @@ impl TryFrom for BackupKeys { } } +/// Dehydrated device key +#[derive(uniffi::Record, Clone)] +pub struct DehydratedDeviceKey { + pub(crate) inner: Vec, +} + +impl DehydratedDeviceKey { + /// Generates a new random pickle key. + pub fn new() -> Result { + let inner = InnerDehydratedDeviceKey::new()?; + Ok(inner.into()) + } + + /// Creates a new dehydration pickle key from the given slice. + /// + /// Fail if the slice length is not 32. + pub fn from_slice(slice: &[u8]) -> Result { + let inner = InnerDehydratedDeviceKey::from_slice(slice)?; + Ok(inner.into()) + } + + /// Export the [`DehydratedDeviceKey`] as a base64 encoded string. + pub fn to_base64(&self) -> String { + let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap(); + inner.to_base64() + } +} +impl From for DehydratedDeviceKey { + fn from(pickle_key: InnerDehydratedDeviceKey) -> Self { + DehydratedDeviceKey { inner: pickle_key.into() } + } +} + impl From for RoomKeyCounts { fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self { Self { total: count.total as i64, backed_up: count.backed_up as i64 } diff --git a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs index 5231e1f4418..2b81bfe3477 100644 --- a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs +++ b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs @@ -69,6 +69,10 @@ pub enum DehydrationError { #[error(transparent)] Pickle(#[from] LibolmPickleError), + /// The pickle key has an invalid length + #[error("The pickle key has an invalid length, expected 32 bytes, got {0}")] + PickleKeyLength(usize), + /// The dehydrated device could not be signed by our user identity, /// we're missing the self-signing key. #[error("The self-signing key is missing, can't create a dehydrated device")] diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 0fa133824c8..38458c13553 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -96,7 +96,10 @@ use matrix_sdk_common::{store_locks::CrossProcessStoreLock, timeout::timeout}; pub use memorystore::MemoryStore; pub use traits::{CryptoStore, DynCryptoStore, IntoCryptoStore}; -pub use crate::gossiping::{GossipRequest, SecretInfo}; +pub use crate::{ + dehydrated_devices::DehydrationError, + gossiping::{GossipRequest, SecretInfo}, +}; /// A wrapper for our CryptoStore trait object. /// @@ -775,6 +778,19 @@ impl DehydratedDeviceKey { Ok(Self { inner: key }) } + /// Creates a new dehydration pickle key from the given slice. + /// + /// Fail if the slice length is not 32. + pub fn from_slice(slice: &[u8]) -> Result { + if slice.len() == 32 { + let mut key = Box::new([0u8; 32]); + key.copy_from_slice(slice); + Ok(DehydratedDeviceKey { inner: key }) + } else { + Err(DehydrationError::PickleKeyLength(slice.len())) + } + } + /// 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]); @@ -789,6 +805,18 @@ impl DehydratedDeviceKey { } } +impl From<&[u8; 32]> for DehydratedDeviceKey { + fn from(value: &[u8; 32]) -> Self { + DehydratedDeviceKey { inner: Box::new(*value) } + } +} + +impl From for Vec { + fn from(key: DehydratedDeviceKey) -> Self { + key.inner.to_vec() + } +} + #[cfg(not(tarpaulin_include))] impl Debug for DehydratedDeviceKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {