feat: Add forwarder_data to InboundGroupSession and pickle.

- Introduces `forwarder_data` to IGS and its pickled form, and a
  helper method to import them from `HistoricRoomKey`s.

Issue: https://github.com/matrix-org/matrix-rust-sdk/issues/5109

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
Richard van der Hoff
2025-12-09 14:05:19 +00:00
committed by Skye Elliot
parent ea43e3f5a8
commit f753d478fa
11 changed files with 165 additions and 41 deletions

View File

@@ -506,6 +506,7 @@ fn collect_sessions(
})
.collect::<anyhow::Result<_>>()?,
sender_data: SenderData::legacy(),
forwarder_data: None,
room_id: RoomId::parse(session.room_id)?,
imported: session.imported,
backed_up: session.backed_up,

View File

@@ -450,6 +450,7 @@ async fn test_verification_states_multiple_device() {
fake_room_id,
&olm,
SenderData::unknown(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
@@ -468,6 +469,7 @@ async fn test_verification_states_multiple_device() {
fake_room_id,
&olm,
SenderData::unknown(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,

View File

@@ -234,6 +234,7 @@ impl StaticAccountData {
room_id,
&outbound.session_key().await,
own_sender_data,
None,
algorithm,
Some(visibility),
shared_history,

View File

@@ -187,6 +187,11 @@ pub struct InboundGroupSession {
/// key.
pub sender_data: SenderData,
/// If this session was shared-on-invite as part of an MSC4268 key bundle,
/// information about the user who forwarded us the session information.
/// This is distinct from [`InboundGroupSession::sender_data`].
pub forwarder_data: Option<SenderData>,
/// The Room this GroupSession belongs to
pub room_id: OwnedRoomId,
@@ -240,6 +245,10 @@ impl InboundGroupSession {
/// * `sender_data` - Information about the sender of the to-device message
/// that established this session.
///
/// * `forwarder_data` - If present, indicates this session was received via
/// an MSC4268 room key bundle, and provides information about the
/// forwarder of this bundle.
///
/// * `encryption_algorithm` - The [`EventEncryptionAlgorithm`] that should
/// be used when messages are being decrypted. The method will return an
/// [`SessionCreationError::Algorithm`] error if an algorithm we do not
@@ -263,6 +272,7 @@ impl InboundGroupSession {
room_id: &RoomId,
session_key: &SessionKey,
sender_data: SenderData,
forwarder_data: Option<SenderData>,
encryption_algorithm: EventEncryptionAlgorithm,
history_visibility: Option<HistoryVisibility>,
shared_history: bool,
@@ -286,6 +296,7 @@ impl InboundGroupSession {
signing_keys: keys.into(),
},
sender_data,
forwarder_data,
room_id: room_id.into(),
imported: false,
algorithm: encryption_algorithm.into(),
@@ -325,6 +336,7 @@ impl InboundGroupSession {
room_id,
session_key,
SenderData::unknown(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
*shared_history,
@@ -380,6 +392,7 @@ impl InboundGroupSession {
sender_key: self.creator_info.curve25519_key,
signing_key: (*self.creator_info.signing_keys).clone(),
sender_data: self.sender_data.clone(),
forwarder_data: self.forwarder_data.clone(),
room_id: self.room_id().to_owned(),
imported: self.imported,
backed_up: self.backed_up(),
@@ -459,6 +472,7 @@ impl InboundGroupSession {
sender_key,
signing_key,
sender_data,
forwarder_data,
room_id,
imported,
backed_up,
@@ -479,6 +493,7 @@ impl InboundGroupSession {
signing_keys: signing_key.into(),
},
sender_data,
forwarder_data,
history_visibility: history_visibility.into(),
first_known_index,
room_id,
@@ -691,6 +706,9 @@ pub struct PickledInboundGroupSession {
/// Information on the device/sender who sent us this session
#[serde(default)]
pub sender_data: SenderData,
/// Information on the device/sender who forwarded us this session
#[serde(default)]
pub forwarder_data: Option<SenderData>,
/// The id of the room that the session is used in.
pub room_id: OwnedRoomId,
/// Flag remembering if the session was directly sent to us by the sender
@@ -717,6 +735,67 @@ fn default_algorithm() -> EventEncryptionAlgorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2
}
impl HistoricRoomKey {
/// Converts a `HistoricRoomKey` into an `InboundGroupSession`.
///
/// This method takes the current `HistoricRoomKey` instance and attempts to
/// create an `InboundGroupSession` from it. The `forwarder_data` parameter
/// provides information about the user or device that forwarded the session
/// information. This is distinct from the original sender of the session.
///
/// # Arguments
///
/// * `forwarder_data` - A reference to a `SenderData` object containing
/// information about the forwarder of the session.
///
/// # Returns
///
/// Returns a `Result` containing the newly created `InboundGroupSession` on
/// success, or a `SessionCreationError` if the conversion fails.
///
/// # Errors
///
/// This method will return a `SessionCreationError` if the session
/// configuration for the given algorithm cannot be determined.
pub fn try_into_inbound_group_session(
&self,
forwarder_data: &SenderData,
) -> Result<InboundGroupSession, SessionCreationError> {
let HistoricRoomKey {
algorithm,
room_id,
sender_key,
session_id,
session_key,
sender_claimed_keys,
} = self;
let config = OutboundGroupSession::session_config(algorithm)?;
let session = InnerSession::import(session_key, config);
let first_known_index = session.first_known_index();
Ok(InboundGroupSession {
inner: Mutex::new(session).into(),
session_id: session_id.to_owned().into(),
creator_info: SessionCreatorInfo {
curve25519_key: *sender_key,
signing_keys: sender_claimed_keys.to_owned().into(),
},
// TODO: How do we remember that this is a historic room key and events decrypted using
// this room key should always show some form of warning.
sender_data: SenderData::default(),
forwarder_data: Some(forwarder_data.clone()),
history_visibility: None.into(),
first_known_index,
room_id: room_id.to_owned(),
imported: true,
algorithm: algorithm.to_owned().into(),
backed_up: AtomicBool::from(false).into(),
shared_history: true,
})
}
}
impl TryFrom<&HistoricRoomKey> for InboundGroupSession {
type Error = SessionCreationError;
@@ -744,6 +823,7 @@ impl TryFrom<&HistoricRoomKey> for InboundGroupSession {
// TODO: How do we remember that this is a historic room key and events decrypted using
// this room key should always show some form of warning.
sender_data: SenderData::default(),
forwarder_data: None,
history_visibility: None.into(),
first_known_index,
room_id: room_id.to_owned(),
@@ -784,6 +864,7 @@ impl TryFrom<&ExportedRoomKey> for InboundGroupSession {
// TODO: In future, exported keys should contain sender data that we can use here.
// See https://github.com/matrix-org/matrix-rust-sdk/issues/3548
sender_data: SenderData::default(),
forwarder_data: None,
history_visibility: None.into(),
first_known_index,
room_id: room_id.to_owned(),
@@ -815,6 +896,7 @@ impl From<&ForwardedMegolmV1AesSha2Content> for InboundGroupSession {
// In future, exported keys should contain sender data that we can use here.
// See https://github.com/matrix-org/matrix-rust-sdk/issues/3548
sender_data: SenderData::default(),
forwarder_data: None,
history_visibility: None.into(),
first_known_index,
room_id: value.room_id.to_owned(),
@@ -842,6 +924,7 @@ impl From<&ForwardedMegolmV2AesSha2Content> for InboundGroupSession {
// In future, exported keys should contain sender data that we can use here.
// See https://github.com/matrix-org/matrix-rust-sdk/issues/3548
sender_data: SenderData::default(),
forwarder_data: None,
history_visibility: None.into(),
first_known_index,
room_id: value.room_id.to_owned(),
@@ -982,6 +1065,7 @@ mod tests {
room_id!("!test:localhost"),
&create_session_key(),
SenderData::unknown(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
Some(HistoryVisibility::Shared),
false,

View File

@@ -826,6 +826,7 @@ mod tests {
room_id,
&session_key,
SenderData::unknown(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,

View File

@@ -1430,6 +1430,7 @@ macro_rules! cryptostore_integration_tests {
room_id!("!r:s.co"),
&session_key,
sender_data,
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,

View File

@@ -847,6 +847,7 @@ mod tests {
room_id,
&outbound.session_key().await,
SenderData::unknown(),
None,
outbound.settings().algorithm.to_owned(),
None,
false,
@@ -1245,6 +1246,7 @@ mod tests {
room_id,
&outbound.session_key().await,
SenderData::unknown(),
None,
outbound.settings().algorithm.to_owned(),
None,
false,

View File

@@ -1445,6 +1445,8 @@ impl Store {
/// * `from_backup_version` - If the keys came from key backup, the key
/// backup version. This will cause the keys to be marked as already
/// backed up, and therefore not requiring another backup.
/// * `forwarder_data` - If the sessions were received as part of an MSC4268
/// key bundle, the information about the user who sent us the bundle.
/// * `progress_listener` - Callback which will be called after each key is
/// processed. Called with arguments `(processed, total)` where
/// `processed` is the number of keys processed so far, and `total` is the
@@ -1455,7 +1457,22 @@ impl Store {
from_backup_version: Option<&str>,
progress_listener: impl Fn(usize, usize),
) -> Result<RoomKeyImportResult> {
let exported_keys: Vec<&ExportedRoomKey> = exported_keys.iter().collect();
let exported_keys: Vec<_> = exported_keys
.iter()
.filter_map(|key| {
key.try_into()
.map_err(|e| {
warn!(
sender_key = key.sender_key().to_base64(),
room_id = ?key.room_id(),
session_id = key.session_id(),
error = ?e,
"Couldn't import a room key from a file export."
);
})
.ok()
})
.collect();
self.import_sessions_impl(exported_keys, from_backup_version, progress_listener).await
}
@@ -1493,57 +1510,43 @@ impl Store {
self.import_room_keys(exported_keys, None, progress_listener).await
}
async fn import_sessions_impl<T>(
async fn import_sessions_impl(
&self,
room_keys: Vec<T>,
sessions: Vec<InboundGroupSession>,
from_backup_version: Option<&str>,
progress_listener: impl Fn(usize, usize),
) -> Result<RoomKeyImportResult>
where
T: TryInto<InboundGroupSession> + RoomKeyExport + Copy,
T::Error: Debug,
{
let mut sessions = Vec::new();
) -> Result<RoomKeyImportResult> {
let mut imported_sessions = Vec::new();
let total_count = room_keys.len();
let total_count = sessions.len();
let mut keys = BTreeMap::new();
for (i, key) in room_keys.into_iter().enumerate() {
match key.try_into() {
Ok(session) => {
// Only import the session if we didn't have this session or
// if it's a better version of the same session.
if let Some(merged) = self.merge_received_group_session(session).await? {
if from_backup_version.is_some() {
merged.mark_as_backed_up();
}
keys.entry(merged.room_id().to_owned())
.or_insert_with(BTreeMap::new)
.entry(merged.sender_key().to_base64())
.or_insert_with(BTreeSet::new)
.insert(merged.session_id().to_owned());
sessions.push(merged);
}
}
Err(e) => {
warn!(
sender_key = key.sender_key().to_base64(),
room_id = ?key.room_id(),
session_id = key.session_id(),
error = ?e,
"Couldn't import a room key from a file export."
);
for (i, session) in sessions.into_iter().enumerate() {
// Only import the session if we didn't have this session or
// if it's a better version of the same session.
if let Some(merged) = self.merge_received_group_session(session).await? {
if from_backup_version.is_some() {
merged.mark_as_backed_up();
}
keys.entry(merged.room_id().to_owned())
.or_insert_with(BTreeMap::new)
.entry(merged.sender_key().to_base64())
.or_insert_with(BTreeSet::new)
.insert(merged.session_id().to_owned());
imported_sessions.push(merged);
}
progress_listener(i, total_count);
}
let imported_count = sessions.len();
let imported_count = imported_sessions.len();
self.inner.store.save_inbound_group_sessions(sessions, from_backup_version).await?;
self.inner
.store
.save_inbound_group_sessions(imported_sessions, from_backup_version)
.await?;
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
@@ -1706,6 +1709,9 @@ impl Store {
tracing::Span::current().record("sender_data", tracing::field::debug(&sender_data));
// The sender's device must be either `SenderData::SenderUnverified` (i.e.,
// TOFU-trusted) or `SenderData::SenderVerified` (i.e., fully verified
// via user verification and cross-signing).
if matches!(
&sender_data,
SenderData::UnknownDevice { .. }
@@ -1718,7 +1724,8 @@ impl Store {
return Ok(());
}
self.import_room_key_bundle_sessions(bundle_info, &bundle, progress_listener).await?;
self.import_room_key_bundle_sessions(bundle_info, &bundle, &sender_data, progress_listener)
.await?;
self.import_room_key_bundle_withheld_info(bundle_info, &bundle).await?;
Ok(())
@@ -1728,6 +1735,7 @@ impl Store {
&self,
bundle_info: &StoredRoomKeyBundleData,
bundle: &RoomKeyBundle,
forwarder_data: &SenderData,
progress_listener: impl Fn(usize, usize),
) -> Result<(), CryptoStoreError> {
let (good, bad): (Vec<_>, Vec<_>) = bundle.room_keys.iter().partition_map(|key| {
@@ -1768,7 +1776,24 @@ impl Store {
);
}
self.import_sessions_impl(good, None, progress_listener).await?;
let keys = good
.iter()
.filter_map(|key| {
key.try_into_inbound_group_session(forwarder_data)
.map_err(|e| {
warn!(
sender_key = ?key.sender_key().to_base64(),
room_id = ?key.room_id(),
session_id = key.session_id(),
error = ?e,
"Couldn't import a room key from a key bundle."
);
})
.ok()
})
.collect();
self.import_sessions_impl(keys, None, progress_listener).await?;
}
}
@@ -1985,6 +2010,7 @@ mod tests {
BTreeMap::new(),
crate::types::Signatures::new(),
)),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
Some(ruma::events::room::history_visibility::HistoryVisibility::Shared),
true,
@@ -2430,6 +2456,7 @@ mod tests {
room_id,
session_key,
SenderData::unknown(),
None,
#[cfg(not(feature = "experimental-algorithms"))]
EventEncryptionAlgorithm::MegolmV1AesSha2,
#[cfg(feature = "experimental-algorithms")]

View File

@@ -524,6 +524,7 @@ mod tests {
&room_id,
session_key,
SenderData::unknown(),
None,
encryption_algorithm,
history_visibility,
false,
@@ -683,6 +684,7 @@ mod tests {
)
.unwrap(),
SenderData::legacy(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
@@ -703,6 +705,7 @@ mod tests {
)
.unwrap(),
SenderData::legacy(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,

View File

@@ -2067,6 +2067,7 @@ mod unit_tests {
)
.unwrap(),
sender_data,
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,

View File

@@ -1540,6 +1540,7 @@ async fn inbound_session_from_outbound_session(
room_id,
&outbound_group_session.session_key().await,
SenderData::unknown(),
None,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,