feat(crypto): Add support for the shared_history flag defined in MSC3061

This patch adds support for the `shared_history` flag from MSC3061 to
the `m.room_key` content, exported room keys, and backed-up room keys.

The flag is now persisted in our `InboundGroupSession`. Additionally,
when creating a new `InboundGroupSession`, we ensure the
`shared_history`  flag is set appropriately.

MSC3061: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
This commit is contained in:
Damir Jelić
2025-02-20 12:14:49 +01:00
parent e1d05fa53c
commit bfa89bc73f
15 changed files with 143 additions and 5 deletions

View File

@@ -507,6 +507,7 @@ fn collect_sessions(
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
shared_history: false,
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
};

View File

@@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
### Features
- [**breaking**] Add support for the shared history flag defined in [MSC3061](https://github.com/matrix-org/matrix-spec-proposals/pull/3061).
The shared history flag is now respected when room keys are received as an
`m.room_key` event as well as when they are imported from a backup or a file
export. We also ensure to set the flag when we send out room keys. Due to
this, a new argument to the constructor for `room_key::MegolmV1AesSha2Content`
has been added and `PickledInboundGroupSession` has received a new
`shared_history` field that defaults to `false.`
([#4700](https://github.com/matrix-org/matrix-rust-sdk/pull/4700))
## [0.10.0] - 2025-02-04
### Features

View File

@@ -353,6 +353,7 @@ async fn test_verification_states_multiple_device() {
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
.unwrap();
@@ -370,6 +371,7 @@ async fn test_verification_states_multiple_device() {
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
.unwrap();

View File

@@ -32,7 +32,7 @@ use ruma::{
upload_signatures::v3::{Request as SignatureUploadRequest, SignedKeys},
},
},
events::AnyToDeviceEvent,
events::{room::history_visibility::HistoryVisibility, AnyToDeviceEvent},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm,
OneTimeKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedOneTimeKeyId, OwnedUserId, RoomId,
@@ -220,6 +220,7 @@ impl StaticAccountData {
let sender_key = identity_keys.curve25519;
let signing_key = identity_keys.ed25519;
let shared_history = shared_history_from_history_visibility(&visibility);
let inbound = InboundGroupSession::new(
sender_key,
@@ -229,6 +230,7 @@ impl StaticAccountData {
own_sender_data,
algorithm,
Some(visibility),
shared_history,
)?;
Ok((outbound, inbound))
@@ -1511,6 +1513,34 @@ impl PartialEq for Account {
}
}
/// Calculate the shared history flag from the history visibility as defined in
/// [MSC3061]
///
/// The MSC defines that the shared history flag should be set to true when the
/// history visibility setting is set to `shared` or `world_readable`:
///
/// > A room key is flagged as having been used for shared history when it was
/// > used to encrypt a message while the room's history visibility setting
/// > was set to world_readable or shared.
///
/// In all other cases, even if we encounter a custom history visibility, we
/// should return false:
///
/// > If the client does not have an m.room.history_visibility state event for
/// > the room, or its value is not understood, the client should treat it as if
/// > its value is joined for the purposes of determining whether the key is
/// > used for shared history.
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
pub(crate) fn shared_history_from_history_visibility(
history_visibility: &HistoryVisibility,
) -> bool {
match history_visibility {
HistoryVisibility::Shared | HistoryVisibility::WorldReadable => true,
HistoryVisibility::Invited | HistoryVisibility::Joined | _ => false,
}
}
/// Expand the pickle key for an older version of dehydrated devices
///
/// The `org.matrix.msc3814.v1.olm` variant of dehydrated devices used the

View File

@@ -154,6 +154,13 @@ pub struct InboundGroupSession {
/// Was this room key backed up to the server.
backed_up: Arc<AtomicBool>,
/// Whether this [`InboundGroupSession`] can be shared with users who are
/// invited to the room in the future, allowing access to history, as
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
shared_history: bool,
}
impl InboundGroupSession {
@@ -176,6 +183,7 @@ impl InboundGroupSession {
///
/// * `sender_data` - Information about the sender of the to-device message
/// that established this session.
#[allow(clippy::too_many_arguments)]
pub fn new(
sender_key: Curve25519PublicKey,
signing_key: Ed25519PublicKey,
@@ -184,6 +192,7 @@ impl InboundGroupSession {
sender_data: SenderData,
encryption_algorithm: EventEncryptionAlgorithm,
history_visibility: Option<HistoryVisibility>,
shared_history: bool,
) -> Result<Self, SessionCreationError> {
let config = OutboundGroupSession::session_config(&encryption_algorithm)?;
@@ -208,6 +217,7 @@ impl InboundGroupSession {
imported: false,
algorithm: encryption_algorithm.into(),
backed_up: AtomicBool::new(false).into(),
shared_history,
})
}
@@ -228,7 +238,13 @@ impl InboundGroupSession {
signing_key: Ed25519PublicKey,
content: &room_key::MegolmV1AesSha2Content,
) -> Result<Self, SessionCreationError> {
let room_key::MegolmV1AesSha2Content { room_id, session_id: _, session_key, .. } = content;
let room_key::MegolmV1AesSha2Content {
room_id,
session_id: _,
session_key,
shared_history,
..
} = content;
Self::new(
sender_key,
@@ -238,6 +254,7 @@ impl InboundGroupSession {
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
*shared_history,
)
}
@@ -265,6 +282,7 @@ impl InboundGroupSession {
backed_up: self.backed_up(),
history_visibility: self.history_visibility.as_ref().clone(),
algorithm: (*self.algorithm).to_owned(),
shared_history: self.shared_history,
}
}
@@ -317,6 +335,7 @@ impl InboundGroupSession {
forwarding_curve25519_key_chain: vec![],
sender_claimed_keys: (*self.creator_info.signing_keys).clone(),
session_key,
shared_history: self.shared_history,
}
}
@@ -342,6 +361,7 @@ impl InboundGroupSession {
backed_up,
history_visibility,
algorithm,
shared_history,
} = pickle;
let session: InnerSession = pickle.into();
@@ -362,6 +382,7 @@ impl InboundGroupSession {
backed_up: AtomicBool::from(backed_up).into(),
algorithm: algorithm.into(),
imported,
shared_history,
})
}
@@ -511,6 +532,15 @@ impl InboundGroupSession {
pub fn sender_data_type(&self) -> SenderDataType {
self.sender_data.to_type()
}
/// Whether this [`InboundGroupSession`] can be shared with users who are
/// invited to the room in the future, allowing access to history, as
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
pub fn shared_history(&self) -> bool {
self.shared_history
}
}
#[cfg(not(tarpaulin_include))]
@@ -556,6 +586,13 @@ pub struct PickledInboundGroupSession {
/// The algorithm of this inbound group session.
#[serde(default = "default_algorithm")]
pub algorithm: EventEncryptionAlgorithm,
/// Whether this [`InboundGroupSession`] can be shared with users who are
/// invited to the room in the future, allowing access to history, as
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default)]
pub shared_history: bool,
}
fn default_algorithm() -> EventEncryptionAlgorithm {
@@ -574,6 +611,7 @@ impl TryFrom<&ExportedRoomKey> for InboundGroupSession {
session_key,
sender_claimed_keys,
forwarding_curve25519_key_chain: _,
shared_history,
} = key;
let config = OutboundGroupSession::session_config(algorithm)?;
@@ -596,6 +634,7 @@ impl TryFrom<&ExportedRoomKey> for InboundGroupSession {
imported: true,
algorithm: algorithm.to_owned().into(),
backed_up: AtomicBool::from(false).into(),
shared_history: *shared_history,
})
}
}
@@ -626,6 +665,7 @@ impl From<&ForwardedMegolmV1AesSha2Content> for InboundGroupSession {
imported: true,
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(),
backed_up: AtomicBool::from(false).into(),
shared_history: false,
}
}
}
@@ -652,6 +692,7 @@ impl From<&ForwardedMegolmV2AesSha2Content> for InboundGroupSession {
imported: true,
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(),
backed_up: AtomicBool::from(false).into(),
shared_history: false,
}
}
}
@@ -768,6 +809,7 @@ mod tests {
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
Some(HistoryVisibility::Shared),
false,
)
.unwrap();
@@ -814,6 +856,7 @@ mod tests {
"room_id":"!test:localhost",
"imported":false,
"backed_up":false,
"shared_history":false,
"history_visibility":"shared",
"algorithm":"m.megolm.v1.aes-sha2"
})

View File

@@ -98,6 +98,14 @@ pub struct ExportedRoomKey {
serialize_with = "serialize_curve_key_vec"
)]
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
/// Whether this [`ExportedRoomKey`] can be shared with users who are
/// invited to the room in the future, allowing access to history, as
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default, rename = "org.matrix.msc3061.shared_history")]
pub shared_history: bool,
}
impl ExportedRoomKey {
@@ -115,6 +123,7 @@ impl ExportedRoomKey {
session_key,
sender_claimed_keys,
forwarding_curve25519_key_chain,
shared_history,
} = room_key;
Self {
@@ -125,6 +134,7 @@ impl ExportedRoomKey {
session_key,
sender_claimed_keys,
forwarding_curve25519_key_chain,
shared_history,
}
}
}
@@ -152,13 +162,21 @@ pub struct BackedUpRoomKey {
pub sender_claimed_keys: SigningKeys<DeviceKeyAlgorithm>,
/// Chain of Curve25519 keys through which this session was forwarded, via
/// m.forwarded_room_key events.
/// `m.forwarded_room_key` events.
#[serde(
default,
deserialize_with = "deserialize_curve_key_vec",
serialize_with = "serialize_curve_key_vec"
)]
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
/// Whether this [`BackedUpRoomKey`] can be shared with users who are
/// invited to the room in the future, allowing access to history, as
/// defined in [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default, rename = "org.matrix.msc3061.shared_history")]
pub shared_history: bool,
}
impl TryFrom<ExportedRoomKey> for ForwardedRoomKeyContent {
@@ -228,6 +246,7 @@ impl From<ExportedRoomKey> for BackedUpRoomKey {
session_key,
sender_claimed_keys,
forwarding_curve25519_key_chain,
shared_history,
} = value;
Self {
@@ -236,6 +255,7 @@ impl From<ExportedRoomKey> for BackedUpRoomKey {
session_key,
sender_claimed_keys,
forwarding_curve25519_key_chain,
shared_history,
}
}
}
@@ -261,6 +281,7 @@ impl TryFrom<ForwardedRoomKeyContent> for ExportedRoomKey {
sender_claimed_keys,
sender_key: content.claimed_sender_key,
session_key: content.session_key,
shared_history: false,
})
}
#[cfg(feature = "experimental-algorithms")]
@@ -272,6 +293,7 @@ impl TryFrom<ForwardedRoomKeyContent> for ExportedRoomKey {
sender_claimed_keys: content.claimed_signing_keys,
sender_key: content.claimed_sender_key,
session_key: content.session_key,
shared_history: false,
}),
ForwardedRoomKeyContent::Unknown(c) => Err(SessionExportError::Algorithm(c.algorithm)),
}

View File

@@ -47,6 +47,7 @@ use super::SessionCreationError;
#[cfg(feature = "experimental-algorithms")]
use crate::types::events::room::encrypted::MegolmV2AesSha2Content;
use crate::{
olm::account::shared_history_from_history_visibility,
session_manager::CollectStrategy,
store::caches::SequenceNumber,
types::{
@@ -526,12 +527,15 @@ impl OutboundGroupSession {
pub(crate) async fn as_content(&self) -> RoomKeyContent {
let session_key = self.session_key().await;
let shared_history =
shared_history_from_history_visibility(&self.settings.history_visibility);
RoomKeyContent::MegolmV1AesSha2(
MegolmV1AesSha2RoomKeyContent::new(
self.room_id().to_owned(),
self.session_id().to_owned(),
session_key,
shared_history,
)
.into(),
)

View File

@@ -861,6 +861,7 @@ mod tests {
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
.unwrap();
if options.session_is_imported {
@@ -1083,6 +1084,7 @@ mod tests {
room_id.to_owned(),
"mysession".to_owned(),
clone_session_key(session_key),
false,
))),
)
}

View File

@@ -1308,6 +1308,7 @@ macro_rules! cryptostore_integration_tests {
sender_data,
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
.unwrap()
}

View File

@@ -806,6 +806,7 @@ mod tests {
SenderData::unknown(),
outbound.settings().algorithm.to_owned(),
None,
false,
)
.unwrap();
@@ -1203,6 +1204,7 @@ mod tests {
SenderData::unknown(),
outbound.settings().algorithm.to_owned(),
None,
false,
)
.unwrap()
}

View File

@@ -113,6 +113,13 @@ pub struct MegolmV1AesSha2Content {
///
/// [`InboundGroupSession`]: vodozemac::megolm::InboundGroupSession
pub session_key: SessionKey,
/// Whether this room key can be shared with users who are invited to the
/// room in the future, allowing access to history, as defined in
/// [MSC3061].
///
/// [MSC3061]: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
#[serde(default, rename = "org.matrix.msc3061.shared_history")]
pub shared_history: bool,
/// Any other, custom and non-specced fields of the content.
#[serde(flatten)]
other: BTreeMap<String, Value>,
@@ -120,8 +127,13 @@ pub struct MegolmV1AesSha2Content {
impl MegolmV1AesSha2Content {
/// Create a new `m.megolm.v1.aes-sha2` `m.room_key` content.
pub fn new(room_id: OwnedRoomId, session_id: String, session_key: SessionKey) -> Self {
Self { room_id, session_id, session_key, other: Default::default() }
pub fn new(
room_id: OwnedRoomId,
session_id: String,
session_key: SessionKey,
shared_history: bool,
) -> Self {
Self { room_id, session_id, session_key, other: Default::default(), shared_history }
}
}
@@ -206,6 +218,7 @@ impl Serialize for RoomKeyContent {
pub(super) mod tests {
use assert_matches::assert_matches;
use serde_json::{json, Value};
use similar_asserts::assert_eq;
use super::RoomKeyEvent;
use crate::types::events::room_key::RoomKeyContent;
@@ -217,6 +230,7 @@ pub(super) mod tests {
"m.custom": "something custom",
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!Cuyf34gef24t:localhost",
"org.matrix.msc3061.shared_history": false,
"session_id": "ZFD6+OmV7fVCsJ7Gap8UnORH8EnmiAkes8FAvQuCw/I",
"session_key": "AgAAAADNp1EbxXYOGmJtyX4AkD1bvJvAUyPkbIaKxtnGKjv\
SQ3E/4mnuqdM4vsmNzpO1EeWzz1rDkUpYhYE9kP7sJhgLXi\

View File

@@ -408,6 +408,7 @@ impl Serialize for ToDeviceEvents {
mod tests {
use assert_matches::assert_matches;
use serde_json::{json, Value};
use similar_asserts::assert_eq;
use super::ToDeviceEvents;

View File

@@ -478,6 +478,7 @@ mod tests {
SenderData::unknown(),
encryption_algorithm,
history_visibility,
false,
)
.unwrap()
}
@@ -636,6 +637,7 @@ mod tests {
SenderData::legacy(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
.unwrap();
backed_up_session.mark_as_backed_up();
@@ -655,6 +657,7 @@ mod tests {
SenderData::legacy(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
.unwrap();

View File

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

View File

@@ -1586,6 +1586,7 @@ async fn inbound_session_from_outbound_session(
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
false,
)
}