Skip to content

crypto: support for building key bundles #4775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ use super::{
BackedUpRoomKey, ExportedRoomKey, OutboundGroupSession, SenderData, SenderDataType,
SessionCreationError, SessionKey,
};
#[cfg(doc)]
use crate::types::{events::room_key::RoomKeyContent, room_history::HistoricRoomKey};
use crate::{
error::{EventError, MegolmResult},
types::{
Expand Down Expand Up @@ -107,6 +109,60 @@ pub(crate) struct SessionCreatorInfo {
/// access of the vodozemac type.
///
/// [vodozemac]: https://matrix-org.github.io/vodozemac/vodozemac/index.html
///
/// ## Structures representing serialised versions of an `InboundGroupSession`
///
/// This crate contains a number of structures which are used for exporting or
/// sharing `InboundGroupSession` between users or devices, in different
/// circumstances. The following is an attempt to catalogue them.
///
/// 1. First, we have the contents of an `m.room_key` to-device message (i.e., a
/// [`RoomKeyContent`]. `RoomKeyContent` is unusual in that it can be created
/// only by the original creator of the session (i.e., someone in possession
/// of the corresponding [`OutboundGroupSession`]), since the embedded
/// `session_key` is self-signed.
///
/// `RoomKeyContent` does **not** include any information about the creator
/// of the session (such as the creator's public device keys), since it is
/// assumed that the original creator of the session is the same as the
/// device sending the to-device message; it is therefore implied by the Olm
/// channel used to send the message.
///
/// All the other structs in this list include a `sender_key` field which
/// contains the Curve25519 key belonging to the device which created the
/// Megolm session (at least, according to the creator of the struct); they
/// also include the Ed25519 key, though the exact serialisation mechanism
/// varies.
///
/// 2. Next, we have the contents of an `m.forwarded_room_key` message (i.e. a
/// [`ForwardedRoomKeyContent`]). This modifies `RoomKeyContent` by (a) using
/// a `session_key` which is not self-signed, (b) adding a `sender_key` field
/// as mentioned above, (c) adding a `sender_claimed_ed25519_key` field
/// containing the original sender's Ed25519 key; (d) adding a
/// `forwarding_curve25519_key_chain` field, which is intended to be used
/// when the key is re-forwarded, but in practice is of little use.
///
/// 3. [`ExportedRoomKey`] is very similar to `ForwardedRoomKeyContent`. The
/// only difference is that the original sender's Ed25519 key is embedded in
/// a `sender_claimed_keys` map rather than a top-level
/// `sender_claimed_ed25519_key` field.
Comment on lines +145 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost forgot about this, now I'm sad again.

///
/// 4. [`BackedUpRoomKey`] is essentially the same as `ExportedRoomKey`, but
/// lacks explicit `room_id` and `session_id` (since those are implied by
/// other parts of the key backup structure).
///
/// 5. [`HistoricRoomKey`] is also similar to `ExportedRoomKey`, but omits
/// `forwarding_curve25519_key_chain` (since it has not been useful in
/// practice) and `shared_history` (because any key being shared via that
/// mechanism is inherently suitable for sharing with other users).
///
/// | Type | Self-signed room key | `room_id`, `session_id` | `sender_key` | Sender's Ed25519 key | `forwarding _curve25519 _key _chain` | `shared _history` |
/// |----------|----------------------|-------------------------|--------------|----------------------|------------------------------------|------------------|
/// | [`RoomKeyContent`] | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
/// | [`ForwardedRoomKeyContent`] | ❌ | ✅ | ✅ | `sender_claimed_ed25519_key` | ✅ | ✅ |
/// | [`ExportedRoomKey`] | ❌ | ✅ | ✅ | `sender_claimed_keys` | ✅ | ✅ |
/// | [`BackedUpRoomKey`] | ❌ | ❌ | ✅ | `sender_claimed_keys` | ✅ | ✅ |
/// | [`HistoricRoomKey`] | ❌ | ✅ | ✅ | `sender_claimed_keys` | ❌ | ❌ |
#[derive(Clone)]
pub struct InboundGroupSession {
inner: Arc<Mutex<InnerSession>>,
Expand Down
8 changes: 6 additions & 2 deletions crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ pub enum SessionExportError {
MissingEd25519Key,
}

/// An exported version of an `InboundGroupSession`
/// An exported version of an [`InboundGroupSession`].
///
/// This can be used to share the `InboundGroupSession` in an exported file.
///
/// See <https://spec.matrix.org/v1.13/client-server-api/#key-export-format>.
#[derive(Deserialize, Serialize)]
#[allow(missing_debug_implementations)]
pub struct ExportedRoomKey {
Expand Down Expand Up @@ -144,7 +146,9 @@ impl ExportedRoomKey {
/// This can be used to back up the [`InboundGroupSession`] to the server using
/// [server-side key backups].
///
/// [server-side key backups]: https://spec.matrix.org/unstable/client-server-api/#server-side-key-backups
/// See <https://spec.matrix.org/v1.13/client-server-api/#definition-backedupsessiondata>.
///
/// [server-side key backups]: https://spec.matrix.org/v1.13/client-server-api/#server-side-key-backups
#[derive(Deserialize, Serialize)]
#[allow(missing_debug_implementations)]
pub struct BackedUpRoomKey {
Expand Down
150 changes: 146 additions & 4 deletions crates/matrix-sdk-crypto/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use futures_util::StreamExt;
use matrix_sdk_common::locks::RwLock as StdRwLock;
use ruma::{
encryption::KeyUsage, events::secret::request::SecretName, DeviceId, OwnedDeviceId,
OwnedRoomId, OwnedUserId, UserId,
OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use thiserror::Error;
Expand Down Expand Up @@ -94,10 +94,15 @@ pub mod integration_tests;
use caches::{SequenceNumber, UsersForKeyQuery};
pub(crate) use crypto_store_wrapper::CryptoStoreWrapper;
pub use error::{CryptoStoreError, Result};
use matrix_sdk_common::{store_locks::CrossProcessStoreLock, timeout::timeout};
use matrix_sdk_common::{
deserialized_responses::WithheldCode, store_locks::CrossProcessStoreLock, timeout::timeout,
};
pub use memorystore::MemoryStore;
pub use traits::{CryptoStore, DynCryptoStore, IntoCryptoStore};

use crate::types::{
events::room_key_withheld::RoomKeyWithheldContent, room_history::RoomKeyBundle,
};
pub use crate::{
dehydrated_devices::DehydrationError,
gossiping::{GossipRequest, SecretInfo},
Expand Down Expand Up @@ -1987,6 +1992,38 @@ impl Store {
Ok(futures_util::stream::iter(sessions.into_iter().filter(predicate))
.then(|session| async move { session.export().await }))
}

/// Assemble a room key bundle for sharing encrypted history, as per
/// [MSC4268].
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub async fn build_room_key_bundle(
&self,
room_id: &RoomId,
) -> std::result::Result<RoomKeyBundle, CryptoStoreError> {
// TODO: make this WAY more efficient. We should only fetch sessions for the
// correct room.
let mut sessions = self.get_inbound_group_sessions().await?;
sessions.retain(|session| session.room_id == room_id);

let mut bundle = RoomKeyBundle::default();
for session in sessions {
if session.shared_history() {
bundle.room_keys.push(session.export().await.into());
} else {
bundle.withheld.push(RoomKeyWithheldContent::new(
session.algorithm().to_owned(),
WithheldCode::Unauthorised,
session.room_id().to_owned(),
session.session_id().to_owned(),
session.sender_key().to_owned(),
self.device_id().to_owned(),
));
}
}

Ok(bundle)
}
}

impl Deref for Store {
Expand Down Expand Up @@ -2021,12 +2058,17 @@ mod tests {
use std::pin::pin;

use futures_util::StreamExt;
use insta::{_macro_support::Content, assert_json_snapshot, internals::ContentPath};
use matrix_sdk_test::async_test;
use ruma::{room_id, user_id};
use ruma::{device_id, room_id, user_id, RoomId};
use vodozemac::megolm::SessionKey;

use crate::{
machine::test_helpers::get_machine_pair, store::DehydratedDeviceKey,
machine::test_helpers::get_machine_pair,
olm::{InboundGroupSession, SenderData},
store::DehydratedDeviceKey,
types::EventEncryptionAlgorithm,
OlmMachine,
};

#[async_test]
Expand Down Expand Up @@ -2190,4 +2232,104 @@ mod tests {

assert!(pickle_key.is_err());
}

#[async_test]
async fn test_build_room_key_bundle() {
// Given: Alice has sent a number of room keys to Bob, including some in the
// wrong room, and some that are not marked as shared...
let alice = OlmMachine::new(user_id!("@a:s.co"), device_id!("ALICE")).await;
let bob = OlmMachine::new(user_id!("@b:s.co"), device_id!("BOB")).await;

let room1_id = room_id!("!room1:localhost");
let room2_id = room_id!("!room2:localhost");

/* We use hardcoded megolm session data, to get a stable output snapshot. These were all created with:

println!("{}", vodozemac::megolm::GroupSession::new(Default::default()).session_key().to_base64());
*/
let session_key1 = "AgAAAAC2XHVzsMBKs4QCRElJ92CJKyGtknCSC8HY7cQ7UYwndMKLQAejXLh5UA0l6s736mgctcUMNvELScUWrObdflrHo+vth/gWreXOaCnaSxmyjjKErQwyIYTkUfqbHy40RJfEesLwnN23on9XAkch/iy8R2+Jz7B8zfG01f2Ow2SxPQFnAndcO1ZSD2GmXgedy6n4B20MWI1jGP2wiexOWbFSya8DO/VxC9m5+/mF+WwYqdpKn9g4Y05Yw4uz7cdjTc3rXm7xK+8E7hI//5QD1nHPvuKYbjjM9u2JSL+Bzp61Cw";
let session_key2 = "AgAAAAC1BXreFTUQQSBGekTEuYxhdytRKyv4JgDGcG+VOBYdPNGgs807SdibCGJky4lJ3I+7ZDGHoUzZPZP/4ogGu4kxni0PWdtWuN7+5zsuamgoFF/BkaGeUUGv6kgIkx8pyPpM5SASTUEP9bN2loDSpUPYwfiIqz74DgC4WQ4435sTBctYvKz8n+TDJwdLXpyT6zKljuqADAioud+s/iqx9LYn9HpbBfezZcvbg67GtE113pLrvde3IcPI5s6dNHK2onGO2B2eoaobcen18bbEDnlUGPeIivArLya7Da6us14jBQ";
let session_key3 = "AgAAAAAM9KFsliaUUhGSXgwOzM5UemjkNH4n8NHgvC/y8hhw13zTF+ooGD4uIYEXYX630oNvQm/EvgZo+dkoc0re+vsqsx4sQeNODdSjcBsWOa0oDF+irQn9oYoLUDPI1IBtY1rX+FV99Zm/xnG7uFOX7aTVlko2GSdejy1w9mfobmfxu5aUc04A9zaKJP1pOthZvRAlhpymGYHgsDtWPrrjyc/yypMflE4kIUEEEtu1kT6mrAmcl615XYRAHYK9G2+fZsGvokwzbkl4nulGwcZMpQEoM0nD2o3GWgX81HW3nGfKBg";
let session_key4 = "AgAAAAA4Kkesxq2h4v9PLD6Sm3Smxspz1PXTqytQPCMQMkkrHNmzV2bHlJ+6/Al9cu8vh1Oj69AK0WUAeJOJuaiskEeg/PI3P03+UYLeC379RzgqwSHdBgdQ41G2vD6zpgmE/8vYToe+qpCZACtPOswZxyqxHH+T/Iq0nv13JmlFGIeA6fEPfr5Y28B49viG74Fs9rxV9EH5PfjbuPM/p+Sz5obShuaBPKQBX1jT913nEXPoIJ06exNZGr0285nw/LgVvNlmWmbqNnbzO2cNZjQWA+xZYz5FSfyCxwqEBbEdUCuRCQ";

let sessions = [
create_inbound_group_session_with_visibility(
&alice,
room1_id,
&SessionKey::from_base64(session_key1).unwrap(),
true,
),
create_inbound_group_session_with_visibility(
&alice,
room1_id,
&SessionKey::from_base64(session_key2).unwrap(),
true,
),
create_inbound_group_session_with_visibility(
&alice,
room1_id,
&SessionKey::from_base64(session_key3).unwrap(),
false,
),
create_inbound_group_session_with_visibility(
&alice,
room2_id,
&SessionKey::from_base64(session_key4).unwrap(),
true,
),
];
bob.store().save_inbound_group_sessions(&sessions).await.unwrap();

// When I build the bundle
let mut bundle = bob.store().build_room_key_bundle(room1_id).await.unwrap();

// Then the bundle matches the snapshot.

// We sort the sessions in the bundle, so that the snapshot is stable.
bundle.room_keys.sort_by_key(|session| session.session_id.clone());

// We also substitute alice's keys in the snapshot with placeholders
let alice_curve_key = alice.identity_keys().curve25519.to_base64();
let map_alice_curve_key = move |value: Content, _path: ContentPath<'_>| {
assert_eq!(value.as_str().unwrap(), alice_curve_key);
"[alice curve key]"
};
let alice_ed25519_key = alice.identity_keys().ed25519.to_base64();
let map_alice_ed25519_key = move |value: Content, _path: ContentPath<'_>| {
assert_eq!(value.as_str().unwrap(), alice_ed25519_key);
"[alice ed25519 key]"
};

insta::with_settings!({ sort_maps => true }, {
assert_json_snapshot!(bundle, {
".room_keys[].sender_key" => insta::dynamic_redaction(map_alice_curve_key.clone()),
".withheld[].sender_key" => insta::dynamic_redaction(map_alice_curve_key),
".room_keys[].sender_claimed_keys.ed25519" => insta::dynamic_redaction(map_alice_ed25519_key),
});
});
}

/// Create an inbound Megolm session for the given room.
///
/// `olm_machine` is used to set the `sender_key` and `signing_key`
/// fields of the resultant session.
fn create_inbound_group_session_with_visibility(
olm_machine: &OlmMachine,
room_id: &RoomId,
session_key: &SessionKey,
shared_history: bool,
) -> InboundGroupSession {
let identity_keys = &olm_machine.store().static_account().identity_keys;
InboundGroupSession::new(
identity_keys.curve25519,
identity_keys.ed25519,
room_id,
session_key,
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
None,
shared_history,
)
.unwrap()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: crates/matrix-sdk-crypto/src/store/mod.rs
expression: bundle
---
{
"room_keys": [
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room1:localhost",
"sender_key": "[alice curve key]",
"session_id": "AWcCd1w7VlIPYaZeB53LqfgHbQxYjWMY/bCJ7E5ZsVI",
"session_key": "AQAAAAC2XHVzsMBKs4QCRElJ92CJKyGtknCSC8HY7cQ7UYwndMKLQAejXLh5UA0l6s736mgctcUMNvELScUWrObdflrHo+vth/gWreXOaCnaSxmyjjKErQwyIYTkUfqbHy40RJfEesLwnN23on9XAkch/iy8R2+Jz7B8zfG01f2Ow2SxPQFnAndcO1ZSD2GmXgedy6n4B20MWI1jGP2wiexOWbFS",
"sender_claimed_keys": {
"ed25519": "[alice ed25519 key]"
}
},
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room1:localhost",
"sender_key": "[alice curve key]",
"session_id": "y1i8rPyf5MMnB0tenJPrMqWO6oAMCKi536z+KrH0tic",
"session_key": "AQAAAAC1BXreFTUQQSBGekTEuYxhdytRKyv4JgDGcG+VOBYdPNGgs807SdibCGJky4lJ3I+7ZDGHoUzZPZP/4ogGu4kxni0PWdtWuN7+5zsuamgoFF/BkaGeUUGv6kgIkx8pyPpM5SASTUEP9bN2loDSpUPYwfiIqz74DgC4WQ4435sTBctYvKz8n+TDJwdLXpyT6zKljuqADAioud+s/iqx9LYn",
"sender_claimed_keys": {
"ed25519": "[alice ed25519 key]"
}
}
],
"withheld": [
{
"algorithm": "m.megolm.v1.aes-sha2",
"code": "m.unauthorised",
"from_device": "BOB",
"reason": "You are not authorised to read the message.",
"room_id": "!room1:localhost",
"sender_key": "[alice curve key]",
"session_id": "lpRzTgD3Nook/Wk62Fm9ECWGnKYZgeCwO1Y+uuPJz/I"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use serde_json::Value;
use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKey};

use super::{EventType, ToDeviceEvent};
#[cfg(doc)]
use crate::olm::InboundGroupSession;
use crate::types::{
deserialize_curve_key, deserialize_curve_key_vec, deserialize_ed25519_key, serialize_curve_key,
serialize_curve_key_vec, serialize_ed25519_key, EventEncryptionAlgorithm, SigningKeys,
Expand All @@ -39,11 +41,15 @@ impl ForwardedRoomKeyEvent {

/// The `m.forwarded_room_key` event content.
///
/// This is an enum over the different room key algorithms we support.
/// This is an enum over the different room key algorithms we support. The
/// currently-supported implementations are used to share
/// [`InboundGroupSession`]s.
///
/// This event type is used to forward keys for end-to-end encryption.
/// Typically it is encrypted as an m.room.encrypted event, then sent as a
/// Typically, it is encrypted as an m.room.encrypted event, then sent as a
/// to-device event.
///
/// See <https://spec.matrix.org/v1.13/client-server-api/#mforwarded_room_key>.
#[derive(Debug, Deserialize)]
#[serde(try_from = "RoomKeyHelper")]
pub enum ForwardedRoomKeyContent {
Expand Down
10 changes: 8 additions & 2 deletions crates/matrix-sdk-crypto/src/types/events/room_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use serde_json::{value::to_raw_value, Value};
use vodozemac::megolm::SessionKey;

use super::{EventType, ToDeviceEvent};
#[cfg(doc)]
use crate::olm::InboundGroupSession;
use crate::types::EventEncryptionAlgorithm;

/// The `m.room_key` to-device event.
Expand All @@ -40,11 +42,15 @@ impl EventType for RoomKeyContent {

/// The `m.room_key` event content.
///
/// This is an enum over the different room key algorithms we support.
/// This is an enum over the different room key algorithms we support. The
/// currently-supported implementations are used to share
/// [`InboundGroupSession`]s.
///
/// This event type is used to exchange keys for end-to-end encryption.
/// Typically it is encrypted as an m.room.encrypted event, then sent as a
/// Typically, it is encrypted as an m.room.encrypted event, then sent as a
/// to-device event.
///
/// See <https://spec.matrix.org/v1.13/client-server-api/#mroom_key>.
#[derive(Debug, Deserialize)]
#[serde(try_from = "RoomKeyHelper")]
pub enum RoomKeyContent {
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk-crypto/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub mod events;
mod one_time_keys;
pub mod qr_login;
pub mod requests;
pub mod room_history;

pub use self::{backup::*, cross_signing::*, device_keys::*, one_time_keys::*};
use crate::store::BackupDecryptionKey;
Expand Down
Loading
Loading