mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-07 15:33:45 -04:00
Embed device keys in Olm-encrypted messages (#3517)
This patch implements MSC4147[1]. Signed-off-by: Hubert Chathi <hubertc@matrix.org> [1]: https://github.com/matrix-org/matrix-spec-proposals/pull/4147
This commit is contained in:
@@ -16,7 +16,11 @@ mod responses;
|
||||
mod users;
|
||||
mod verification;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
pub use backup_recovery_key::{
|
||||
@@ -33,7 +37,9 @@ use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
@@ -43,8 +49,8 @@ pub use responses::{
|
||||
};
|
||||
use ruma::{
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId,
|
||||
SecondsSinceUnixEpoch, UserId,
|
||||
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
|
||||
RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
@@ -332,6 +338,10 @@ async fn save_changes(
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
// The Sessions were created with incorrect device keys, so clear the cache
|
||||
// so that they'll get recreated with correct ones.
|
||||
store.clear_caches().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -419,6 +429,27 @@ fn collect_sessions(
|
||||
) -> anyhow::Result<(Vec<Session>, Vec<InboundGroupSession>)> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
// Create a DeviceKeys struct with enough information to get a working
|
||||
// Session, but we will won't actually use the Sessions (and we'll clear
|
||||
// the session cache after migration) so we don't need to worry about
|
||||
// signatures.
|
||||
let device_keys = DeviceKeys::new(
|
||||
user_id.clone(),
|
||||
device_id.clone(),
|
||||
Default::default(),
|
||||
BTreeMap::from([
|
||||
(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &device_id),
|
||||
DeviceKey::Ed25519(identity_keys.ed25519),
|
||||
),
|
||||
(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Curve25519, &device_id),
|
||||
DeviceKey::Curve25519(identity_keys.curve25519),
|
||||
),
|
||||
]),
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
for session_pickle in session_pickles {
|
||||
let pickle =
|
||||
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
|
||||
@@ -439,8 +470,7 @@ fn collect_sessions(
|
||||
last_use_time,
|
||||
};
|
||||
|
||||
let session =
|
||||
Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle);
|
||||
let session = Session::from_pickle(device_keys.clone(), pickle)?;
|
||||
|
||||
sessions.push(session);
|
||||
processed_steps += 1;
|
||||
|
||||
@@ -30,6 +30,7 @@ testing = ["dep:http"]
|
||||
[dependencies]
|
||||
aes = "0.8.1"
|
||||
as_variant = { workspace = true }
|
||||
assert_matches2 = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bs58 = { version = "0.5.0" }
|
||||
byteorder = { workspace = true }
|
||||
|
||||
@@ -165,6 +165,19 @@ pub enum EventError {
|
||||
MismatchedRoom(OwnedRoomId, Option<OwnedRoomId>),
|
||||
}
|
||||
|
||||
/// Error type describing different errors that can happen when we create an
|
||||
/// Olm session from a pickle.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SessionUnpickleError {
|
||||
/// The device keys are missing the signing key
|
||||
#[error("the device keys are missing the signing key")]
|
||||
MissingSigningKey,
|
||||
|
||||
/// The device keys are missing the identity key
|
||||
#[error("the device keys are missing the identity key")]
|
||||
MissingIdentityKey,
|
||||
}
|
||||
|
||||
/// Error type describing different errors that happen when we check or create
|
||||
/// signatures for a Matrix JSON object.
|
||||
#[derive(Error, Debug)]
|
||||
|
||||
@@ -871,7 +871,8 @@ impl ReadOnlyDevice {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_device_keys(&self) -> &DeviceKeys {
|
||||
/// Return the device keys
|
||||
pub fn as_device_keys(&self) -> &DeviceKeys {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,14 @@ impl OlmMachine {
|
||||
let static_account = account.static_data().clone();
|
||||
|
||||
let store = Arc::new(CryptoStoreWrapper::new(self.user_id(), MemoryStore::new()));
|
||||
let device = ReadOnlyDevice::from_account(&account);
|
||||
store.save_pending_changes(PendingChanges { account: Some(account) }).await?;
|
||||
store
|
||||
.save_changes(Changes {
|
||||
devices: DeviceChanges { new: vec![device], ..Default::default() },
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Self::new_helper(
|
||||
device_id,
|
||||
|
||||
@@ -905,12 +905,16 @@ impl Account {
|
||||
/// and shared with us.
|
||||
///
|
||||
/// * `fallback_used` - Was the one-time key a fallback key.
|
||||
///
|
||||
/// * `our_device_keys` - Our own `DeviceKeys`, including cross-signing
|
||||
/// signatures if applicable, for embedding in encrypted messages.
|
||||
pub fn create_outbound_session_helper(
|
||||
&self,
|
||||
config: SessionConfig,
|
||||
identity_key: Curve25519PublicKey,
|
||||
one_time_key: Curve25519PublicKey,
|
||||
fallback_used: bool,
|
||||
our_device_keys: DeviceKeys,
|
||||
) -> Session {
|
||||
let session = self.inner.create_outbound_session(config, identity_key, one_time_key);
|
||||
|
||||
@@ -918,12 +922,10 @@ impl Account {
|
||||
let session_id = session.session_id();
|
||||
|
||||
Session {
|
||||
user_id: self.static_data.user_id.clone(),
|
||||
device_id: self.static_data.device_id.clone(),
|
||||
our_identity_keys: self.static_data.identity_keys.clone(),
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: session_id.into(),
|
||||
sender_key: identity_key,
|
||||
our_device_keys,
|
||||
created_using_fallback_key: fallback_used,
|
||||
creation_time: now,
|
||||
last_use_time: now,
|
||||
@@ -978,11 +980,15 @@ impl Account {
|
||||
///
|
||||
/// * `key_map` - A map from the algorithm and device ID to the one-time key
|
||||
/// that the other account created and shared with us.
|
||||
///
|
||||
/// * `our_device_keys` - Our own `DeviceKeys`, including cross-signing
|
||||
/// signatures if applicable, for embedding in encrypted messages.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn create_outbound_session(
|
||||
&self,
|
||||
device: &ReadOnlyDevice,
|
||||
key_map: &BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>>,
|
||||
our_device_keys: DeviceKeys,
|
||||
) -> Result<Session, SessionCreationError> {
|
||||
let pre_key_bundle = Self::find_pre_key_bundle(device, key_map)?;
|
||||
|
||||
@@ -1012,6 +1018,7 @@ impl Account {
|
||||
identity_key,
|
||||
one_time_key,
|
||||
is_fallback,
|
||||
our_device_keys,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1026,11 +1033,15 @@ impl Account {
|
||||
///
|
||||
/// * `their_identity_key` - The other account's identity/curve25519 key.
|
||||
///
|
||||
/// * `our_device_keys` - Our own `DeviceKeys`, including cross-signing
|
||||
/// signatures if applicable, for embedding in encrypted messages.
|
||||
///
|
||||
/// * `message` - A pre-key Olm message that was sent to us by the other
|
||||
/// account.
|
||||
pub fn create_inbound_session(
|
||||
&mut self,
|
||||
their_identity_key: Curve25519PublicKey,
|
||||
our_device_keys: DeviceKeys,
|
||||
message: &PreKeyMessage,
|
||||
) -> Result<InboundCreationResult, SessionCreationError> {
|
||||
Span::current().record("session_id", debug(message.session_id()));
|
||||
@@ -1043,12 +1054,10 @@ impl Account {
|
||||
debug!(session=?result.session, "Decrypted an Olm message from a new Olm session");
|
||||
|
||||
let session = Session {
|
||||
user_id: self.static_data.user_id.clone(),
|
||||
device_id: self.static_data.device_id.clone(),
|
||||
our_identity_keys: self.static_data.identity_keys.clone(),
|
||||
inner: Arc::new(Mutex::new(result.session)),
|
||||
session_id: session_id.into(),
|
||||
sender_key: their_identity_key,
|
||||
our_device_keys,
|
||||
created_using_fallback_key: false,
|
||||
creation_time: now,
|
||||
last_use_time: now,
|
||||
@@ -1072,7 +1081,8 @@ impl Account {
|
||||
let one_time_map = other.signed_one_time_keys();
|
||||
let device = ReadOnlyDevice::from_account(other);
|
||||
|
||||
let mut our_session = self.create_outbound_session(&device, &one_time_map).unwrap();
|
||||
let mut our_session =
|
||||
self.create_outbound_session(&device, &one_time_map, self.device_keys()).unwrap();
|
||||
|
||||
other.mark_keys_as_published();
|
||||
|
||||
@@ -1104,8 +1114,13 @@ impl Account {
|
||||
};
|
||||
|
||||
let our_device = ReadOnlyDevice::from_account(self);
|
||||
let other_session =
|
||||
other.create_inbound_session(our_device.curve25519_key().unwrap(), &prekey).unwrap();
|
||||
let other_session = other
|
||||
.create_inbound_session(
|
||||
our_device.curve25519_key().unwrap(),
|
||||
other.device_keys(),
|
||||
&prekey,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(our_session, other_session.session)
|
||||
}
|
||||
@@ -1290,20 +1305,23 @@ impl Account {
|
||||
);
|
||||
|
||||
return Err(OlmError::SessionWedged(
|
||||
session.user_id.to_owned(),
|
||||
session.our_device_keys.user_id.to_owned(),
|
||||
session.sender_key(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a matching session; try to create a new session.
|
||||
let result = match self.create_inbound_session(sender_key, prekey_message) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!("Failed to create a new Olm session from a pre-key message: {e:?}");
|
||||
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key));
|
||||
}
|
||||
};
|
||||
let device_keys = store.get_own_device().await?.as_device_keys().clone();
|
||||
let result =
|
||||
match self.create_inbound_session(sender_key, device_keys, prekey_message) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to create a new Olm session from a pre-key message: {e:?}"
|
||||
);
|
||||
return Err(OlmError::SessionWedged(sender.to_owned(), sender_key));
|
||||
}
|
||||
};
|
||||
|
||||
// We need to add the new session to the session cache, otherwise
|
||||
// we might try to create the same session again.
|
||||
|
||||
@@ -93,6 +93,7 @@ pub(crate) mod tests {
|
||||
sender_key,
|
||||
one_time_key,
|
||||
false,
|
||||
alice.device_keys(),
|
||||
);
|
||||
|
||||
(alice, session)
|
||||
@@ -144,6 +145,7 @@ pub(crate) mod tests {
|
||||
alice_keys.curve25519,
|
||||
one_time_key,
|
||||
false,
|
||||
bob.device_keys(),
|
||||
);
|
||||
|
||||
let plaintext = "Hello world";
|
||||
@@ -156,7 +158,9 @@ pub(crate) mod tests {
|
||||
};
|
||||
|
||||
let bob_keys = bob.identity_keys();
|
||||
let result = alice.create_inbound_session(bob_keys.curve25519, &prekey_message).unwrap();
|
||||
let result = alice
|
||||
.create_inbound_session(bob_keys.curve25519, alice.device_keys(), &prekey_message)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(bob_session.session_id(), result.session.session_id());
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use ruma::{serde::Raw, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
|
||||
use ruma::{serde::Raw, SecondsSinceUnixEpoch};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -24,14 +24,13 @@ use vodozemac::{
|
||||
Curve25519PublicKey,
|
||||
};
|
||||
|
||||
use super::IdentityKeys;
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
use crate::types::events::room::encrypted::OlmV2Curve25519AesSha2Content;
|
||||
use crate::{
|
||||
error::{EventError, OlmResult},
|
||||
error::{EventError, OlmResult, SessionUnpickleError},
|
||||
types::{
|
||||
events::room::encrypted::{OlmV1Curve25519AesSha2Content, ToDeviceEncryptedEventContent},
|
||||
EventEncryptionAlgorithm,
|
||||
DeviceKeys, EventEncryptionAlgorithm,
|
||||
},
|
||||
ReadOnlyDevice,
|
||||
};
|
||||
@@ -40,18 +39,14 @@ use crate::{
|
||||
/// `Account`s
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
/// The `UserId` associated with this session
|
||||
pub user_id: OwnedUserId,
|
||||
/// The specific `DeviceId` associated with this session
|
||||
pub device_id: OwnedDeviceId,
|
||||
/// The `IdentityKeys` associated with this session
|
||||
pub our_identity_keys: Arc<IdentityKeys>,
|
||||
/// The OlmSession
|
||||
pub inner: Arc<Mutex<InnerSession>>,
|
||||
/// Our sessionId
|
||||
pub session_id: Arc<str>,
|
||||
/// The Key of the sender
|
||||
pub sender_key: Curve25519PublicKey,
|
||||
/// Our own signed device keys
|
||||
pub our_device_keys: DeviceKeys,
|
||||
/// Has this been created using the fallback key
|
||||
pub created_using_fallback_key: bool,
|
||||
/// When the session was created
|
||||
@@ -156,11 +151,12 @@ impl Session {
|
||||
recipient_device.ed25519_key().ok_or(EventError::MissingSigningKey)?;
|
||||
|
||||
let payload = json!({
|
||||
"sender": &self.user_id,
|
||||
"sender_device": &self.device_id,
|
||||
"sender": &self.our_device_keys.user_id,
|
||||
"sender_device": &self.our_device_keys.device_id,
|
||||
"keys": {
|
||||
"ed25519": self.our_identity_keys.ed25519.to_base64(),
|
||||
"ed25519": self.our_device_keys.ed25519_key().expect("Device doesn't have ed25519 key").to_base64(),
|
||||
},
|
||||
"device_keys": self.our_device_keys,
|
||||
"recipient": recipient_device.user_id(),
|
||||
"recipient_keys": {
|
||||
"ed25519": recipient_signing_key.to_base64(),
|
||||
@@ -178,14 +174,20 @@ impl Session {
|
||||
EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => OlmV1Curve25519AesSha2Content {
|
||||
ciphertext,
|
||||
recipient_key: self.sender_key,
|
||||
sender_key: self.our_identity_keys.curve25519,
|
||||
sender_key: self
|
||||
.our_device_keys
|
||||
.curve25519_key()
|
||||
.expect("Device doesn't have curve25519 key"),
|
||||
message_id,
|
||||
}
|
||||
.into(),
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
EventEncryptionAlgorithm::OlmV2Curve25519AesSha2 => OlmV2Curve25519AesSha2Content {
|
||||
ciphertext,
|
||||
sender_key: self.our_identity_keys.curve25519,
|
||||
sender_key: self
|
||||
.our_device_keys
|
||||
.curve25519_key()
|
||||
.expect("Device doesn't have curve25519 key"),
|
||||
message_id,
|
||||
}
|
||||
.into(),
|
||||
@@ -227,36 +229,32 @@ impl Session {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - Our own user id that the session belongs to.
|
||||
///
|
||||
/// * `device_id` - Our own device ID that the session belongs to.
|
||||
///
|
||||
/// * `our_identity_keys` - An clone of the Arc to our own identity keys.
|
||||
/// * `our_device_keys` - Our own signed device keys.
|
||||
///
|
||||
/// * `pickle` - The pickled version of the `Session`.
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
pub fn from_pickle(
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
our_identity_keys: Arc<IdentityKeys>,
|
||||
our_device_keys: DeviceKeys,
|
||||
pickle: PickledSession,
|
||||
) -> Self {
|
||||
) -> Result<Self, SessionUnpickleError> {
|
||||
if our_device_keys.curve25519_key().is_none() {
|
||||
return Err(SessionUnpickleError::MissingIdentityKey);
|
||||
}
|
||||
if our_device_keys.ed25519_key().is_none() {
|
||||
return Err(SessionUnpickleError::MissingSigningKey);
|
||||
}
|
||||
|
||||
let session: vodozemac::olm::Session = pickle.pickle.into();
|
||||
let session_id = session.session_id();
|
||||
|
||||
Session {
|
||||
user_id,
|
||||
device_id,
|
||||
our_identity_keys,
|
||||
Ok(Session {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: session_id.into(),
|
||||
created_using_fallback_key: pickle.created_using_fallback_key,
|
||||
sender_key: pickle.sender_key,
|
||||
our_device_keys,
|
||||
creation_time: pickle.creation_time,
|
||||
last_use_time: pickle.last_use_time,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,3 +283,73 @@ pub struct PickledSession {
|
||||
/// The Unix timestamp when the session was last used.
|
||||
pub last_use_time: SecondsSinceUnixEpoch,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{device_id, user_id};
|
||||
use serde_json::{self, Value};
|
||||
use vodozemac::olm::{OlmMessage, SessionConfig};
|
||||
|
||||
use crate::{
|
||||
identities::ReadOnlyDevice, olm::Account,
|
||||
types::events::room::encrypted::ToDeviceEncryptedEventContent,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
async fn test_encryption_and_decryption() {
|
||||
use ruma::events::dummy::ToDeviceDummyEventContent;
|
||||
|
||||
// Given users Alice and Bob
|
||||
let alice =
|
||||
Account::with_device_id(user_id!("@alice:localhost"), device_id!("ALICEDEVICE"));
|
||||
let mut bob = Account::with_device_id(user_id!("@bob:localhost"), device_id!("BOBDEVICE"));
|
||||
|
||||
// When Alice creates an Olm session with Bob
|
||||
bob.generate_one_time_keys(1);
|
||||
let one_time_key = *bob.one_time_keys().values().next().unwrap();
|
||||
let sender_key = bob.identity_keys().curve25519;
|
||||
let mut alice_session = alice.create_outbound_session_helper(
|
||||
SessionConfig::default(),
|
||||
sender_key,
|
||||
one_time_key,
|
||||
false,
|
||||
alice.device_keys(),
|
||||
);
|
||||
|
||||
let alice_device = ReadOnlyDevice::from_account(&alice);
|
||||
|
||||
// and encrypts a message
|
||||
let message = alice_session
|
||||
.encrypt(&alice_device, "m.dummy", ToDeviceDummyEventContent::new(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
assert_let!(ToDeviceEncryptedEventContent::OlmV2Curve25519AesSha2(content) = message);
|
||||
#[cfg(not(feature = "experimental-algorithms"))]
|
||||
assert_let!(ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(content) = message);
|
||||
|
||||
let prekey = if let OlmMessage::PreKey(m) = content.ciphertext {
|
||||
m
|
||||
} else {
|
||||
panic!("Wrong Olm message type");
|
||||
};
|
||||
|
||||
// Then Bob should be able to create a session from the message and decrypt it.
|
||||
let bob_session_result = bob
|
||||
.create_inbound_session(
|
||||
alice_device.curve25519_key().unwrap(),
|
||||
bob.device_keys(),
|
||||
&prekey,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Also ensure that the encrypted payload has the device keys.
|
||||
let plaintext: Value = serde_json::from_str(&bob_session_result.plaintext).unwrap();
|
||||
assert_eq!(plaintext["device_keys"]["user_id"].as_str(), Some("@alice:localhost"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +537,8 @@ impl SessionManager {
|
||||
};
|
||||
|
||||
let account = store_transaction.account().await?;
|
||||
let session = match account.create_outbound_session(&device, key_map) {
|
||||
let device_keys = self.store.get_own_device().await?.as_device_keys().clone();
|
||||
let session = match account.create_outbound_session(&device, key_map, device_keys) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -631,7 +632,7 @@ mod tests {
|
||||
identities::{IdentityManager, ReadOnlyDevice},
|
||||
olm::{Account, PrivateCrossSigningIdentity},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
|
||||
store::{Changes, CryptoStoreWrapper, DeviceChanges, MemoryStore, PendingChanges, Store},
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
|
||||
@@ -688,7 +689,15 @@ mod tests {
|
||||
);
|
||||
|
||||
let store = Store::new(account.static_data().clone(), identity, store, verification);
|
||||
let device = ReadOnlyDevice::from_account(&account);
|
||||
store.save_pending_changes(PendingChanges { account: Some(account) }).await.unwrap();
|
||||
store
|
||||
.save_changes(Changes {
|
||||
devices: DeviceChanges { new: vec![device], ..Default::default() },
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session_cache = GroupSessionCache::new(store.clone());
|
||||
let identity_manager = IdentityManager::new(store.clone());
|
||||
|
||||
@@ -85,6 +85,7 @@ macro_rules! cryptostore_integration_tests {
|
||||
sender_key,
|
||||
one_time_key,
|
||||
false,
|
||||
alice.device_keys(),
|
||||
);
|
||||
|
||||
(alice, session)
|
||||
@@ -216,6 +217,16 @@ macro_rules! cryptostore_integration_tests {
|
||||
})
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
store
|
||||
.save_changes(Changes {
|
||||
devices: DeviceChanges {
|
||||
new: vec![ReadOnlyDevice::from_account(&account)],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
|
||||
store.save_changes(changes).await.unwrap();
|
||||
|
||||
@@ -474,6 +474,12 @@ impl CryptoStore for MemoryStore {
|
||||
Ok(self.devices.user_devices(user_id))
|
||||
}
|
||||
|
||||
async fn get_own_device(&self) -> Result<ReadOnlyDevice> {
|
||||
let account = self.load_account().await?.unwrap();
|
||||
|
||||
Ok(self.devices.get(&account.user_id, &account.device_id).unwrap())
|
||||
}
|
||||
|
||||
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<ReadOnlyUserIdentities>> {
|
||||
Ok(self.identities.read().unwrap().get(user_id).cloned())
|
||||
}
|
||||
@@ -1265,6 +1271,10 @@ mod integration_tests {
|
||||
self.0.get_user_devices(user_id).await
|
||||
}
|
||||
|
||||
async fn get_own_device(&self) -> Result<ReadOnlyDevice, Self::Error> {
|
||||
self.0.get_own_device().await
|
||||
}
|
||||
|
||||
async fn get_user_identity(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
|
||||
@@ -203,6 +203,12 @@ pub trait CryptoStore: AsyncTraitDeps {
|
||||
user_id: &UserId,
|
||||
) -> Result<HashMap<OwnedDeviceId, ReadOnlyDevice>, Self::Error>;
|
||||
|
||||
/// Get the device for the current client.
|
||||
///
|
||||
/// Since our own device is set when the store is created, this will always
|
||||
/// return a device (unless there is an error).
|
||||
async fn get_own_device(&self) -> Result<ReadOnlyDevice, Self::Error>;
|
||||
|
||||
/// Get the user identity that is attached to the given user id.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -450,6 +456,10 @@ impl<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
|
||||
self.0.get_user_devices(user_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_own_device(&self) -> Result<ReadOnlyDevice> {
|
||||
self.0.get_own_device().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<ReadOnlyUserIdentities>> {
|
||||
self.0.get_user_identity(user_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -447,7 +447,14 @@ impl IndexeddbCryptoStore {
|
||||
|
||||
/// Process all the changes and do all encryption/serialization before the
|
||||
/// actual transaction.
|
||||
async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
|
||||
///
|
||||
/// Returns a tuple where the first item is a `PendingIndexeddbChanges`
|
||||
/// struct, and the second item is a boolean indicating whether the session
|
||||
/// cache should be cleared.
|
||||
async fn prepare_for_transaction(
|
||||
&self,
|
||||
changes: &Changes,
|
||||
) -> Result<(PendingIndexeddbChanges, bool)> {
|
||||
let mut indexeddb_changes = PendingIndexeddbChanges::new();
|
||||
|
||||
let private_identity_pickle =
|
||||
@@ -534,7 +541,17 @@ impl IndexeddbCryptoStore {
|
||||
|
||||
let mut device_store = indexeddb_changes.get(keys::DEVICES);
|
||||
|
||||
let account_info = self.get_static_account();
|
||||
let mut clear_caches = false;
|
||||
for device in device_changes.new.iter().chain(&device_changes.changed) {
|
||||
// If our own device key changes, we need to clear the session
|
||||
// cache because the sessions contain a copy of our device key, and
|
||||
// we want the sessions to use the new version.
|
||||
if account_info.as_ref().is_some_and(|info| {
|
||||
info.user_id == device.user_id() && info.device_id == device.device_id()
|
||||
}) {
|
||||
clear_caches = true;
|
||||
}
|
||||
let key =
|
||||
self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
|
||||
let device = self.serializer.serialize_value(&device)?;
|
||||
@@ -617,7 +634,7 @@ impl IndexeddbCryptoStore {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(indexeddb_changes)
|
||||
Ok((indexeddb_changes, clear_caches))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,7 +714,7 @@ impl_crypto_store! {
|
||||
// TODO: #2000 should make this lock go away, or change its shape.
|
||||
let _guard = self.save_changes_lock.lock().await;
|
||||
|
||||
let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
|
||||
let (indexeddb_changes, clear_caches) = self.prepare_for_transaction(&changes).await?;
|
||||
|
||||
let stores = indexeddb_changes.touched_stores();
|
||||
|
||||
@@ -713,9 +730,15 @@ impl_crypto_store! {
|
||||
|
||||
tx.await.into_result()?;
|
||||
|
||||
// all good, let's update our caches:indexeddb
|
||||
for session in changes.sessions {
|
||||
self.session_cache.add(session).await;
|
||||
if clear_caches {
|
||||
self.clear_caches().await;
|
||||
} else {
|
||||
// All good, let's update our caches:indexeddb.
|
||||
// We only do this if clear_caches is false, because the sessions may
|
||||
// have been created using old device_keys.
|
||||
for session in changes.sessions {
|
||||
self.session_cache.add(session).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -860,9 +883,12 @@ impl_crypto_store! {
|
||||
}
|
||||
|
||||
async fn get_sessions(&self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
|
||||
let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
|
||||
|
||||
if self.session_cache.get(sender_key).is_none() {
|
||||
let device_keys = self.get_own_device()
|
||||
.await?
|
||||
.as_device_keys()
|
||||
.clone();
|
||||
|
||||
let range = self.serializer.encode_to_range(keys::SESSION, sender_key)?;
|
||||
let sessions: Vec<Session> = self
|
||||
.inner
|
||||
@@ -873,13 +899,12 @@ impl_crypto_store! {
|
||||
.iter()
|
||||
.filter_map(|f| self.serializer.deserialize_value(f).ok().map(|p| {
|
||||
Session::from_pickle(
|
||||
account_info.user_id.clone(),
|
||||
account_info.device_id.clone(),
|
||||
account_info.identity_keys.clone(),
|
||||
device_keys.clone(),
|
||||
p,
|
||||
)
|
||||
.map_err(|_| IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset))
|
||||
}))
|
||||
.collect::<Vec<Session>>();
|
||||
.collect::<Result<Vec<Session>>>()?;
|
||||
|
||||
self.session_cache.set_for_sender(sender_key, sessions);
|
||||
}
|
||||
@@ -1094,6 +1119,13 @@ impl_crypto_store! {
|
||||
.collect::<HashMap<_, _>>())
|
||||
}
|
||||
|
||||
async fn get_own_device(&self) -> Result<ReadOnlyDevice> {
|
||||
let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
|
||||
Ok(self.get_device(&account_info.user_id, &account_info.device_id)
|
||||
.await?
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<ReadOnlyUserIdentities>> {
|
||||
Ok(self
|
||||
.inner
|
||||
@@ -1692,7 +1724,12 @@ mod wasm_unit_tests {
|
||||
|
||||
#[cfg(all(test, target_arch = "wasm32"))]
|
||||
mod tests {
|
||||
use matrix_sdk_crypto::cryptostore_integration_tests;
|
||||
use matrix_sdk_crypto::{
|
||||
cryptostore_integration_tests,
|
||||
store::{Changes, CryptoStore as _, DeviceChanges, PendingChanges},
|
||||
ReadOnlyDevice,
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use super::IndexeddbCryptoStore;
|
||||
|
||||
@@ -1708,6 +1745,43 @@ mod tests {
|
||||
.expect("Can't create store without passphrase"),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn cache_cleared_after_device_update() {
|
||||
let store = get_store("cache_cleared_after_device_update", None).await;
|
||||
// Given we created a session and saved it in the store
|
||||
let (account, session) = cryptostore_integration_tests::get_account_and_session().await;
|
||||
let sender_key = session.sender_key.to_base64();
|
||||
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
|
||||
store.save_changes(changes).await.unwrap();
|
||||
|
||||
store.session_cache.get(&sender_key).expect("We should have a session");
|
||||
|
||||
// When we save a new version of our device keys
|
||||
store
|
||||
.save_changes(Changes {
|
||||
devices: DeviceChanges {
|
||||
new: vec![ReadOnlyDevice::from_account(&account)],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Then the session is no longer in the cache
|
||||
assert!(
|
||||
store.session_cache.get(&sender_key).is_none(),
|
||||
"Session should not be in the cache!"
|
||||
);
|
||||
}
|
||||
|
||||
cryptostore_integration_tests!();
|
||||
}
|
||||
|
||||
|
||||
@@ -780,9 +780,11 @@ impl CryptoStore for SqliteCryptoStore {
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
self.acquire()
|
||||
let clear_caches = self
|
||||
.acquire()
|
||||
.await?
|
||||
.with_transaction(move |txn| {
|
||||
let mut clear_caches = false;
|
||||
if let Some(pickled_private_identity) = &pickled_private_identity {
|
||||
let serialized_private_identity =
|
||||
this.serialize_value(pickled_private_identity)?;
|
||||
@@ -804,7 +806,16 @@ impl CryptoStore for SqliteCryptoStore {
|
||||
txn.set_kv("backup_version_v1", &serialized_backup_version)?;
|
||||
}
|
||||
|
||||
let account_info = this.get_static_account();
|
||||
for device in changes.devices.new.iter().chain(&changes.devices.changed) {
|
||||
// If our own device key changes, we need to clear the
|
||||
// session cache because the sessions contain a copy of our
|
||||
// device key.
|
||||
if account_info.clone().is_some_and(|info| {
|
||||
info.user_id == device.user_id() && info.device_id == device.device_id()
|
||||
}) {
|
||||
clear_caches = true;
|
||||
}
|
||||
let user_id = this.encode_key("device", device.user_id().as_bytes());
|
||||
let device_id = this.encode_key("device", device.device_id().as_bytes());
|
||||
let data = this.serialize_value(&device)?;
|
||||
@@ -875,10 +886,14 @@ impl CryptoStore for SqliteCryptoStore {
|
||||
txn.set_secret(&secret_name, &value)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
Ok::<_, Error>(clear_caches)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if clear_caches {
|
||||
self.clear_caches().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -905,9 +920,9 @@ impl CryptoStore for SqliteCryptoStore {
|
||||
}
|
||||
|
||||
async fn get_sessions(&self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
|
||||
let account_info = self.get_static_account().ok_or(Error::AccountUnset)?;
|
||||
|
||||
if self.session_cache.get(sender_key).is_none() {
|
||||
let device_keys = self.get_own_device().await?.as_device_keys().clone();
|
||||
|
||||
let sessions = self
|
||||
.acquire()
|
||||
.await?
|
||||
@@ -916,12 +931,8 @@ impl CryptoStore for SqliteCryptoStore {
|
||||
.into_iter()
|
||||
.map(|bytes| {
|
||||
let pickle = self.deserialize_value(&bytes)?;
|
||||
Ok(Session::from_pickle(
|
||||
account_info.user_id.clone(),
|
||||
account_info.device_id.clone(),
|
||||
account_info.identity_keys.clone(),
|
||||
pickle,
|
||||
))
|
||||
Session::from_pickle(device_keys.clone(), pickle)
|
||||
.map_err(|_| Error::AccountUnset)
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
@@ -1110,6 +1121,11 @@ impl CryptoStore for SqliteCryptoStore {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn get_own_device(&self) -> Result<ReadOnlyDevice> {
|
||||
let account_info = self.get_static_account().ok_or(Error::AccountUnset)?;
|
||||
Ok(self.get_device(&account_info.user_id, &account_info.device_id).await?.unwrap())
|
||||
}
|
||||
|
||||
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<ReadOnlyUserIdentities>> {
|
||||
let user_id = self.encode_key("identity", user_id.as_bytes());
|
||||
Ok(self
|
||||
@@ -1331,7 +1347,8 @@ mod tests {
|
||||
mod encrypted_tests {
|
||||
use matrix_sdk_crypto::{
|
||||
cryptostore_integration_tests, cryptostore_integration_tests_time,
|
||||
store::{Changes, CryptoStore as _, PendingChanges},
|
||||
store::{Changes, CryptoStore as _, DeviceChanges, PendingChanges},
|
||||
ReadOnlyDevice,
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -1377,6 +1394,42 @@ mod encrypted_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn cache_cleared_after_device_update() {
|
||||
let store = get_store("cache_cleared_after_device_update", None).await;
|
||||
// Given we created a session and saved it in the store
|
||||
let (account, session) = cryptostore_integration_tests::get_account_and_session().await;
|
||||
let sender_key = session.sender_key.to_base64();
|
||||
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
|
||||
store.save_changes(changes).await.unwrap();
|
||||
|
||||
store.session_cache.get(&sender_key).expect("We should have a session");
|
||||
|
||||
// When we save a new version of our device keys
|
||||
store
|
||||
.save_changes(Changes {
|
||||
devices: DeviceChanges {
|
||||
new: vec![ReadOnlyDevice::from_account(&account)],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Then the session is no longer in the cache
|
||||
assert!(
|
||||
store.session_cache.get(&sender_key).is_none(),
|
||||
"Session should not be in the cache!"
|
||||
);
|
||||
}
|
||||
|
||||
cryptostore_integration_tests!();
|
||||
cryptostore_integration_tests_time!();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user