Skip to content

Commit ccedfb4

Browse files
committed
Ensure correct Olm session selection for encrypted messages
In some cases, restoring client state from backups may cause Olm sessions to become corrupted, resulting in a backward ratchet. As a consequence, the receiving side is unable to decrypt messages. To address this issue, the failed side initiates a new Olm session and sends a dummy encrypted message. However, this dummy message also creates a new Olm session on the sender's side. To ensure that both sides use the same new session, we must sort sessions by their creation timestamp before encrypting new messages. Although the session list was sorted correctly previously, we selected the wrong side of the list, resulting in an outdated session being selected instead of the newest one. This patch resolves the issue by selecting the newest Olm session and adds a test to the session getter logic to prevent reintroduction of this bug.
1 parent 3508253 commit ccedfb4

File tree

2 files changed

+151
-43
lines changed

2 files changed

+151
-43
lines changed

crates/matrix-sdk-crypto/src/identities/device.rs

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ impl Device {
335335
self.verification_machine.store.get_sessions(&k.to_base64()).await
336336
}
337337

338+
#[cfg(test)]
339+
pub(crate) async fn get_most_recent_session(&self) -> OlmResult<Option<Session>> {
340+
self.inner.get_most_recent_session(self.verification_machine.store.inner()).await
341+
}
342+
338343
/// Is this device considered to be verified.
339344
///
340345
/// This method returns true if either [`is_locally_trusted()`] returns true
@@ -612,6 +617,34 @@ impl ReadOnlyDevice {
612617
}
613618
}
614619

620+
/// Find and return the most recently created Olm [`Session`] we are sharing
621+
/// with this device.
622+
pub(crate) async fn get_most_recent_session(
623+
&self,
624+
store: &DynCryptoStore,
625+
) -> OlmResult<Option<Session>> {
626+
if let Some(sender_key) = self.curve25519_key() {
627+
if let Some(s) = store.get_sessions(&sender_key.to_base64()).await? {
628+
let mut sessions = s.lock().await;
629+
630+
sessions.sort_by_key(|s| s.creation_time);
631+
632+
Ok(sessions.last().cloned())
633+
} else {
634+
Ok(None)
635+
}
636+
} else {
637+
warn!(
638+
user_id = ?self.user_id(),
639+
device_id = ?self.device_id(),
640+
"Trying to find a Olm session of a device, but the device doesn't have a \
641+
Curve25519 key",
642+
);
643+
644+
Err(EventError::MissingSenderKey.into())
645+
}
646+
}
647+
615648
/// Does this device support the olm.v2.curve25519-aes-sha2 encryption
616649
/// algorithm.
617650
#[cfg(feature = "experimental-algorithms")]
@@ -680,44 +713,29 @@ impl ReadOnlyDevice {
680713
event_type: &str,
681714
content: Value,
682715
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
683-
let Some(sender_key) = self.curve25519_key() else {
684-
warn!(
716+
let session = self.get_most_recent_session(store).await?;
717+
718+
if let Some(mut session) = session {
719+
let message = session.encrypt(self, event_type, content).await?;
720+
721+
trace!(
685722
user_id = ?self.user_id(),
686723
device_id = ?self.device_id(),
687-
"Trying to encrypt a Megolm session, but the device doesn't \
688-
have a curve25519 key",
724+
session_id = session.session_id(),
725+
"Successfully encrypted a Megolm session",
689726
);
690-
return Err(EventError::MissingSenderKey.into());
691-
};
692727

693-
let session = if let Some(s) = store.get_sessions(&sender_key.to_base64()).await? {
694-
let mut sessions = s.lock().await;
695-
sessions.sort_by_key(|s| s.last_use_time);
696-
sessions.get(0).cloned()
728+
Ok((session, message))
697729
} else {
698-
None
699-
};
700-
701-
let Some(mut session) = session else {
702730
warn!(
703731
"Trying to encrypt a Megolm session for user {} on device {}, \
704732
but no Olm session is found",
705733
self.user_id(),
706734
self.device_id()
707735
);
708-
return Err(OlmError::MissingSession);
709-
};
710-
711-
let message = session.encrypt(self, event_type, content).await?;
712736

713-
trace!(
714-
user_id = ?self.user_id(),
715-
device_id = ?self.device_id(),
716-
session_id = session.session_id(),
717-
"Successfully encrypted a Megolm session",
718-
);
719-
720-
Ok((session, message))
737+
Err(OlmError::MissingSession)
738+
}
721739
}
722740

723741
/// Update a device with a new device keys struct.

crates/matrix-sdk-crypto/src/machine.rs

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,7 +1665,12 @@ pub(crate) mod testing {
16651665

16661666
#[cfg(test)]
16671667
pub(crate) mod tests {
1668-
use std::{collections::BTreeMap, iter, sync::Arc};
1668+
use std::{
1669+
collections::BTreeMap,
1670+
iter,
1671+
sync::Arc,
1672+
time::{Duration, SystemTime},
1673+
};
16691674

16701675
use assert_matches::assert_matches;
16711676
use matrix_sdk_common::deserialized_responses::{
@@ -1695,7 +1700,7 @@ pub(crate) mod tests {
16951700
room_id,
16961701
serde::Raw,
16971702
uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch,
1698-
OwnedDeviceKeyId, TransactionId, UserId,
1703+
OwnedDeviceKeyId, SecondsSinceUnixEpoch, TransactionId, UserId,
16991704
};
17001705
use serde_json::json;
17011706
use vodozemac::{
@@ -2027,23 +2032,34 @@ pub(crate) mod tests {
20272032
assert!(user_sessions.contains_key(alice_device));
20282033
}
20292034

2030-
#[async_test]
2031-
async fn test_session_creation() {
2032-
let (alice_machine, bob_machine, one_time_keys) = get_machine_pair().await;
2033-
2034-
let mut bob_keys = BTreeMap::new();
2035-
2036-
let (device_key_id, one_time_key) = one_time_keys.iter().next().unwrap();
2037-
let mut keys = BTreeMap::new();
2038-
keys.insert(device_key_id.clone(), one_time_key.clone());
2039-
bob_keys.insert(bob_machine.device_id().into(), keys);
2040-
2041-
let mut one_time_keys = BTreeMap::new();
2042-
one_time_keys.insert(bob_machine.user_id().to_owned(), bob_keys);
2043-
2035+
async fn create_session(
2036+
machine: &OlmMachine,
2037+
user_id: &UserId,
2038+
device_id: &DeviceId,
2039+
key_id: OwnedDeviceKeyId,
2040+
one_time_key: Raw<OneTimeKey>,
2041+
) {
2042+
let keys = BTreeMap::from([(key_id, one_time_key)]);
2043+
let keys = BTreeMap::from([(device_id.to_owned(), keys)]);
2044+
let one_time_keys = BTreeMap::from([(user_id.to_owned(), keys)]);
20442045
let response = claim_keys::v3::Response::new(one_time_keys);
20452046

2046-
alice_machine.receive_keys_claim_response(&response).await.unwrap();
2047+
machine.receive_keys_claim_response(&response).await.unwrap();
2048+
}
2049+
2050+
#[async_test]
2051+
async fn test_session_creation() {
2052+
let (alice_machine, bob_machine, mut one_time_keys) = get_machine_pair().await;
2053+
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
2054+
2055+
create_session(
2056+
&alice_machine,
2057+
bob_machine.user_id(),
2058+
bob_machine.device_id(),
2059+
device_key_id,
2060+
one_time_key,
2061+
)
2062+
.await;
20472063

20482064
let session = alice_machine
20492065
.store
@@ -2055,6 +2071,80 @@ pub(crate) mod tests {
20552071
assert!(!session.lock().await.is_empty())
20562072
}
20572073

2074+
#[async_test]
2075+
async fn getting_most_recent_session() {
2076+
let (alice_machine, bob_machine, mut one_time_keys) = get_machine_pair().await;
2077+
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
2078+
2079+
let device = alice_machine
2080+
.get_device(bob_machine.user_id(), bob_machine.device_id(), None)
2081+
.await
2082+
.unwrap()
2083+
.unwrap();
2084+
2085+
assert!(device.get_most_recent_session().await.unwrap().is_none());
2086+
2087+
create_session(
2088+
&alice_machine,
2089+
bob_machine.user_id(),
2090+
bob_machine.device_id(),
2091+
device_key_id,
2092+
one_time_key.to_owned(),
2093+
)
2094+
.await;
2095+
2096+
for _ in 0..10 {
2097+
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
2098+
2099+
create_session(
2100+
&alice_machine,
2101+
bob_machine.user_id(),
2102+
bob_machine.device_id(),
2103+
device_key_id,
2104+
one_time_key.to_owned(),
2105+
)
2106+
.await;
2107+
}
2108+
2109+
// Since the sessions are created quickly in succession and our timestamps have
2110+
// a resolution in seconds, it's very likely that we're going to end up
2111+
// with the same timestamps, so we manually masage them to be 10s apart.
2112+
let session_id = {
2113+
let sessions = alice_machine
2114+
.store
2115+
.get_sessions(&bob_machine.identity_keys().curve25519.to_base64())
2116+
.await
2117+
.unwrap()
2118+
.unwrap();
2119+
2120+
let mut use_time = SystemTime::now();
2121+
let mut sessions = sessions.lock().await;
2122+
2123+
let mut session_id = None;
2124+
2125+
// Iterate through the sessions skipping the first and last element so we know
2126+
// that the correct session isn't the first nor the last one.
2127+
let (_, sessions_slice) = sessions.as_mut_slice().split_last_mut().unwrap();
2128+
2129+
for session in sessions_slice.iter_mut().skip(1) {
2130+
session.creation_time = SecondsSinceUnixEpoch::from_system_time(use_time).unwrap();
2131+
use_time += Duration::from_secs(10);
2132+
2133+
session_id = Some(session.session_id().to_owned());
2134+
}
2135+
2136+
session_id.unwrap()
2137+
};
2138+
2139+
let newest_session = device.get_most_recent_session().await.unwrap().unwrap();
2140+
2141+
assert_eq!(
2142+
newest_session.session_id(),
2143+
session_id,
2144+
"The session we found is the one that was most recently created"
2145+
);
2146+
}
2147+
20582148
#[async_test]
20592149
async fn test_olm_encryption() {
20602150
let (alice, bob) = get_machine_pair_with_session().await;

0 commit comments

Comments
 (0)