Skip to content

Commit 002e776

Browse files
committed
crypto: support for building key bundles
Add a method to CryptoStore which will construct a key bundle, ready for encrypting and sharing with invited users. Part of #4504
1 parent 3d653d3 commit 002e776

File tree

8 files changed

+378
-10
lines changed

8 files changed

+378
-10
lines changed

crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ use super::{
4040
BackedUpRoomKey, ExportedRoomKey, OutboundGroupSession, SenderData, SenderDataType,
4141
SessionCreationError, SessionKey,
4242
};
43+
#[cfg(doc)]
44+
use crate::types::{events::room_key::RoomKeyContent, room_history::HistoricRoomKey};
4345
use crate::{
4446
error::{EventError, MegolmResult},
4547
types::{
@@ -107,6 +109,60 @@ pub(crate) struct SessionCreatorInfo {
107109
/// access of the vodozemac type.
108110
///
109111
/// [vodozemac]: https://matrix-org.github.io/vodozemac/vodozemac/index.html
112+
///
113+
/// ## Structures representing serialised versions of an `InboundGroupSession`
114+
///
115+
/// This crate contains a number of structures which are used for exporting or
116+
/// sharing `InboundGroupSession` between users or devices, in different
117+
/// circumstances. The following is an attempt to catalogue them.
118+
///
119+
/// 1. First, we have the contents of an `m.room_key` to-device message (i.e., a
120+
/// [`RoomKeyContent`]. `RoomKeyContent` is unusual in that it can be created
121+
/// only by the original creator of the session (i.e., someone in possession
122+
/// of the corresponding [`OutboundGroupSession`]), since the embedded
123+
/// `session_key` is self-signed.
124+
///
125+
/// `RoomKeyContent` does **not** include any information about the creator
126+
/// of the session (such as the creator's public device keys), since it is
127+
/// assumed that the original creator of the session is the same as the
128+
/// device sending the to-device message; it is therefore implied by the Olm
129+
/// channel used to send the message.
130+
///
131+
/// All the other structs in this list include a `sender_key` field which
132+
/// contains the Curve25519 key belonging to the device which created the
133+
/// Megolm session (at least, according to the creator of the struct); they
134+
/// also include the Ed25519 key, though the exact serialisation mechanism
135+
/// varies.
136+
///
137+
/// 2. Next, we have the contents of an `m.forwarded_room_key` message (i.e. a
138+
/// [`ForwardedRoomKeyContent`]). This modifies `RoomKeyContent` by (a) using
139+
/// a `session_key` which is not self-signed, (b) adding a `sender_key` field
140+
/// as mentioned above, (c) adding a `sender_claimed_ed25519_key` field
141+
/// containing the original sender's Ed25519 key; (d) adding a
142+
/// `forwarding_curve25519_key_chain` field, which is intended to be used
143+
/// when the key is re-forwarded, but in practice is of little use.
144+
///
145+
/// 3. [`ExportedRoomKey`] is very similar to `ForwardedRoomKeyContent`. The
146+
/// only difference is that the original sender's Ed25519 key is embedded in
147+
/// a `sender_claimed_keys` map rather than a top-level
148+
/// `sender_claimed_ed25519_key` field.
149+
///
150+
/// 4. [`BackedUpRoomKey`] is essentially the same as `ExportedRoomKey`, but
151+
/// lacks explicit `room_id` and `session_id` (since those are implied by
152+
/// other parts of the key backup structure).
153+
///
154+
/// 5. [`HistoricRoomKey`] is also similar to `ExportedRoomKey`, but omits
155+
/// `forwarding_curve25519_key_chain` (since it has not been useful in
156+
/// practice) and `shared_history` (because any key being shared via that
157+
/// mechanism is inherently suitable for sharing with other users).
158+
///
159+
/// | Type | Self-signed room key | `room_id`, `session_id` | `sender_key` | Sender's Ed25519 key | `forwarding _curve25519 _key _chain` | `shared _history` |
160+
/// |----------|----------------------|-------------------------|--------------|----------------------|------------------------------------|------------------|
161+
/// | [`RoomKeyContent`] | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
162+
/// | [`ForwardedRoomKeyContent`] | ❌ | ✅ | ✅ | `sender_claimed_ed25519_key` | ✅ | ✅ |
163+
/// | [`ExportedRoomKey`] | ❌ | ✅ | ✅ | `sender_claimed_keys` | ✅ | ✅ |
164+
/// | [`BackedUpRoomKey`] | ❌ | ❌ | ✅ | `sender_claimed_keys` | ✅ | ✅ |
165+
/// | [`HistoricRoomKey`] | ❌ | ✅ | ✅ | `sender_claimed_keys` | ❌ | ❌ |
110166
#[derive(Clone)]
111167
pub struct InboundGroupSession {
112168
inner: Arc<Mutex<InnerSession>>,

crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ pub enum SessionExportError {
6464
MissingEd25519Key,
6565
}
6666

67-
/// An exported version of an `InboundGroupSession`
67+
/// An exported version of an [`InboundGroupSession`].
6868
///
6969
/// This can be used to share the `InboundGroupSession` in an exported file.
70+
///
71+
/// See <https://spec.matrix.org/v1.13/client-server-api/#key-export-format>.
7072
#[derive(Deserialize, Serialize)]
7173
#[allow(missing_debug_implementations)]
7274
pub struct ExportedRoomKey {
@@ -144,7 +146,9 @@ impl ExportedRoomKey {
144146
/// This can be used to back up the [`InboundGroupSession`] to the server using
145147
/// [server-side key backups].
146148
///
147-
/// [server-side key backups]: https://spec.matrix.org/unstable/client-server-api/#server-side-key-backups
149+
/// See <https://spec.matrix.org/v1.13/client-server-api/#definition-backedupsessiondata>.
150+
///
151+
/// [server-side key backups]: https://spec.matrix.org/v1.13/client-server-api/#server-side-key-backups
148152
#[derive(Deserialize, Serialize)]
149153
#[allow(missing_debug_implementations)]
150154
pub struct BackedUpRoomKey {

crates/matrix-sdk-crypto/src/store/mod.rs

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ use futures_util::StreamExt;
5353
use matrix_sdk_common::locks::RwLock as StdRwLock;
5454
use ruma::{
5555
encryption::KeyUsage, events::secret::request::SecretName, DeviceId, OwnedDeviceId,
56-
OwnedRoomId, OwnedUserId, UserId,
56+
OwnedRoomId, OwnedUserId, RoomId, UserId,
5757
};
5858
use serde::{de::DeserializeOwned, Deserialize, Serialize};
5959
use thiserror::Error;
@@ -94,10 +94,15 @@ pub mod integration_tests;
9494
use caches::{SequenceNumber, UsersForKeyQuery};
9595
pub(crate) use crypto_store_wrapper::CryptoStoreWrapper;
9696
pub use error::{CryptoStoreError, Result};
97-
use matrix_sdk_common::{store_locks::CrossProcessStoreLock, timeout::timeout};
97+
use matrix_sdk_common::{
98+
deserialized_responses::WithheldCode, store_locks::CrossProcessStoreLock, timeout::timeout,
99+
};
98100
pub use memorystore::MemoryStore;
99101
pub use traits::{CryptoStore, DynCryptoStore, IntoCryptoStore};
100102

103+
use crate::types::{
104+
events::room_key_withheld::RoomKeyWithheldContent, room_history::RoomKeyBundle,
105+
};
101106
pub use crate::{
102107
dehydrated_devices::DehydrationError,
103108
gossiping::{GossipRequest, SecretInfo},
@@ -1987,6 +1992,38 @@ impl Store {
19871992
Ok(futures_util::stream::iter(sessions.into_iter().filter(predicate))
19881993
.then(|session| async move { session.export().await }))
19891994
}
1995+
1996+
/// Assemble a room key bundle for sharing encrypted history, as per
1997+
/// [MSC4268].
1998+
///
1999+
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
2000+
pub async fn build_room_key_bundle(
2001+
&self,
2002+
room_id: &RoomId,
2003+
) -> std::result::Result<RoomKeyBundle, CryptoStoreError> {
2004+
// TODO: make this WAY more efficient. We should only fetch sessions for the
2005+
// correct room.
2006+
let mut sessions = self.get_inbound_group_sessions().await?;
2007+
sessions.retain(|session| session.room_id == room_id);
2008+
2009+
let mut bundle = RoomKeyBundle::default();
2010+
for session in sessions {
2011+
if session.shared_history() {
2012+
bundle.room_keys.push(session.export().await.into());
2013+
} else {
2014+
bundle.withheld.push(RoomKeyWithheldContent::new(
2015+
session.algorithm().to_owned(),
2016+
WithheldCode::Unauthorised,
2017+
session.room_id().to_owned(),
2018+
session.session_id().to_owned(),
2019+
session.sender_key().to_owned(),
2020+
self.device_id().to_owned(),
2021+
));
2022+
}
2023+
}
2024+
2025+
Ok(bundle)
2026+
}
19902027
}
19912028

19922029
impl Deref for Store {
@@ -2021,12 +2058,17 @@ mod tests {
20212058
use std::pin::pin;
20222059

20232060
use futures_util::StreamExt;
2061+
use insta::{_macro_support::Content, assert_json_snapshot, internals::ContentPath};
20242062
use matrix_sdk_test::async_test;
2025-
use ruma::{room_id, user_id};
2063+
use ruma::{device_id, room_id, user_id, RoomId};
2064+
use vodozemac::megolm::SessionKey;
20262065

20272066
use crate::{
2028-
machine::test_helpers::get_machine_pair, store::DehydratedDeviceKey,
2067+
machine::test_helpers::get_machine_pair,
2068+
olm::{InboundGroupSession, SenderData},
2069+
store::DehydratedDeviceKey,
20292070
types::EventEncryptionAlgorithm,
2071+
OlmMachine,
20302072
};
20312073

20322074
#[async_test]
@@ -2190,4 +2232,104 @@ mod tests {
21902232

21912233
assert!(pickle_key.is_err());
21922234
}
2235+
2236+
#[async_test]
2237+
async fn test_build_room_key_bundle() {
2238+
// Given: Alice has sent a number of room keys to Bob, including some in the
2239+
// wrong room, and some that are not marked as shared...
2240+
let alice = OlmMachine::new(user_id!("@a:s.co"), device_id!("ALICE")).await;
2241+
let bob = OlmMachine::new(user_id!("@b:s.co"), device_id!("BOB")).await;
2242+
2243+
let room1_id = room_id!("!room1:localhost");
2244+
let room2_id = room_id!("!room2:localhost");
2245+
2246+
/* We use hardcoded megolm session data, to get a stable output snapshot. These were all created with:
2247+
2248+
println!("{}", vodozemac::megolm::GroupSession::new(Default::default()).session_key().to_base64());
2249+
*/
2250+
let session_key1 = "AgAAAAC2XHVzsMBKs4QCRElJ92CJKyGtknCSC8HY7cQ7UYwndMKLQAejXLh5UA0l6s736mgctcUMNvELScUWrObdflrHo+vth/gWreXOaCnaSxmyjjKErQwyIYTkUfqbHy40RJfEesLwnN23on9XAkch/iy8R2+Jz7B8zfG01f2Ow2SxPQFnAndcO1ZSD2GmXgedy6n4B20MWI1jGP2wiexOWbFSya8DO/VxC9m5+/mF+WwYqdpKn9g4Y05Yw4uz7cdjTc3rXm7xK+8E7hI//5QD1nHPvuKYbjjM9u2JSL+Bzp61Cw";
2251+
let session_key2 = "AgAAAAC1BXreFTUQQSBGekTEuYxhdytRKyv4JgDGcG+VOBYdPNGgs807SdibCGJky4lJ3I+7ZDGHoUzZPZP/4ogGu4kxni0PWdtWuN7+5zsuamgoFF/BkaGeUUGv6kgIkx8pyPpM5SASTUEP9bN2loDSpUPYwfiIqz74DgC4WQ4435sTBctYvKz8n+TDJwdLXpyT6zKljuqADAioud+s/iqx9LYn9HpbBfezZcvbg67GtE113pLrvde3IcPI5s6dNHK2onGO2B2eoaobcen18bbEDnlUGPeIivArLya7Da6us14jBQ";
2252+
let session_key3 = "AgAAAAAM9KFsliaUUhGSXgwOzM5UemjkNH4n8NHgvC/y8hhw13zTF+ooGD4uIYEXYX630oNvQm/EvgZo+dkoc0re+vsqsx4sQeNODdSjcBsWOa0oDF+irQn9oYoLUDPI1IBtY1rX+FV99Zm/xnG7uFOX7aTVlko2GSdejy1w9mfobmfxu5aUc04A9zaKJP1pOthZvRAlhpymGYHgsDtWPrrjyc/yypMflE4kIUEEEtu1kT6mrAmcl615XYRAHYK9G2+fZsGvokwzbkl4nulGwcZMpQEoM0nD2o3GWgX81HW3nGfKBg";
2253+
let session_key4 = "AgAAAAA4Kkesxq2h4v9PLD6Sm3Smxspz1PXTqytQPCMQMkkrHNmzV2bHlJ+6/Al9cu8vh1Oj69AK0WUAeJOJuaiskEeg/PI3P03+UYLeC379RzgqwSHdBgdQ41G2vD6zpgmE/8vYToe+qpCZACtPOswZxyqxHH+T/Iq0nv13JmlFGIeA6fEPfr5Y28B49viG74Fs9rxV9EH5PfjbuPM/p+Sz5obShuaBPKQBX1jT913nEXPoIJ06exNZGr0285nw/LgVvNlmWmbqNnbzO2cNZjQWA+xZYz5FSfyCxwqEBbEdUCuRCQ";
2254+
2255+
let sessions = [
2256+
create_inbound_group_session_with_visibility(
2257+
&alice,
2258+
room1_id,
2259+
&SessionKey::from_base64(session_key1).unwrap(),
2260+
true,
2261+
),
2262+
create_inbound_group_session_with_visibility(
2263+
&alice,
2264+
room1_id,
2265+
&SessionKey::from_base64(session_key2).unwrap(),
2266+
true,
2267+
),
2268+
create_inbound_group_session_with_visibility(
2269+
&alice,
2270+
room1_id,
2271+
&SessionKey::from_base64(session_key3).unwrap(),
2272+
false,
2273+
),
2274+
create_inbound_group_session_with_visibility(
2275+
&alice,
2276+
room2_id,
2277+
&SessionKey::from_base64(session_key4).unwrap(),
2278+
true,
2279+
),
2280+
];
2281+
bob.store().save_inbound_group_sessions(&sessions).await.unwrap();
2282+
2283+
// When I build the bundle
2284+
let mut bundle = bob.store().build_room_key_bundle(room1_id).await.unwrap();
2285+
2286+
// Then the bundle matches the snapshot.
2287+
2288+
// We sort the sessions in the bundle, so that the snapshot is stable.
2289+
bundle.room_keys.sort_by_key(|session| session.session_id.clone());
2290+
2291+
// We also substitute alice's keys in the snapshot with placeholders
2292+
let alice_curve_key = alice.identity_keys().curve25519.to_base64();
2293+
let map_alice_curve_key = move |value: Content, _path: ContentPath<'_>| {
2294+
assert_eq!(value.as_str().unwrap(), alice_curve_key);
2295+
"[alice curve key]"
2296+
};
2297+
let alice_ed25519_key = alice.identity_keys().ed25519.to_base64();
2298+
let map_alice_ed25519_key = move |value: Content, _path: ContentPath<'_>| {
2299+
assert_eq!(value.as_str().unwrap(), alice_ed25519_key);
2300+
"[alice ed25519 key]"
2301+
};
2302+
2303+
insta::with_settings!({ sort_maps => true }, {
2304+
assert_json_snapshot!(bundle, {
2305+
".room_keys[].sender_key" => insta::dynamic_redaction(map_alice_curve_key.clone()),
2306+
".withheld[].sender_key" => insta::dynamic_redaction(map_alice_curve_key),
2307+
".room_keys[].sender_claimed_keys.ed25519" => insta::dynamic_redaction(map_alice_ed25519_key),
2308+
});
2309+
});
2310+
}
2311+
2312+
/// Create an inbound Megolm session for the given room.
2313+
///
2314+
/// `olm_machine` is used to set the `sender_key` and `signing_key`
2315+
/// fields of the resultant session.
2316+
fn create_inbound_group_session_with_visibility(
2317+
olm_machine: &OlmMachine,
2318+
room_id: &RoomId,
2319+
session_key: &SessionKey,
2320+
shared_history: bool,
2321+
) -> InboundGroupSession {
2322+
let identity_keys = &olm_machine.store().static_account().identity_keys;
2323+
InboundGroupSession::new(
2324+
identity_keys.curve25519,
2325+
identity_keys.ed25519,
2326+
room_id,
2327+
session_key,
2328+
SenderData::unknown(),
2329+
EventEncryptionAlgorithm::MegolmV1AesSha2,
2330+
None,
2331+
shared_history,
2332+
)
2333+
.unwrap()
2334+
}
21932335
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
source: crates/matrix-sdk-crypto/src/store/mod.rs
3+
expression: bundle
4+
---
5+
{
6+
"room_keys": [
7+
{
8+
"algorithm": "m.megolm.v1.aes-sha2",
9+
"room_id": "!room1:localhost",
10+
"sender_key": "[alice curve key]",
11+
"session_id": "AWcCd1w7VlIPYaZeB53LqfgHbQxYjWMY/bCJ7E5ZsVI",
12+
"session_key": "AQAAAAC2XHVzsMBKs4QCRElJ92CJKyGtknCSC8HY7cQ7UYwndMKLQAejXLh5UA0l6s736mgctcUMNvELScUWrObdflrHo+vth/gWreXOaCnaSxmyjjKErQwyIYTkUfqbHy40RJfEesLwnN23on9XAkch/iy8R2+Jz7B8zfG01f2Ow2SxPQFnAndcO1ZSD2GmXgedy6n4B20MWI1jGP2wiexOWbFS",
13+
"sender_claimed_keys": {
14+
"ed25519": "[alice ed25519 key]"
15+
}
16+
},
17+
{
18+
"algorithm": "m.megolm.v1.aes-sha2",
19+
"room_id": "!room1:localhost",
20+
"sender_key": "[alice curve key]",
21+
"session_id": "y1i8rPyf5MMnB0tenJPrMqWO6oAMCKi536z+KrH0tic",
22+
"session_key": "AQAAAAC1BXreFTUQQSBGekTEuYxhdytRKyv4JgDGcG+VOBYdPNGgs807SdibCGJky4lJ3I+7ZDGHoUzZPZP/4ogGu4kxni0PWdtWuN7+5zsuamgoFF/BkaGeUUGv6kgIkx8pyPpM5SASTUEP9bN2loDSpUPYwfiIqz74DgC4WQ4435sTBctYvKz8n+TDJwdLXpyT6zKljuqADAioud+s/iqx9LYn",
23+
"sender_claimed_keys": {
24+
"ed25519": "[alice ed25519 key]"
25+
}
26+
}
27+
],
28+
"withheld": [
29+
{
30+
"algorithm": "m.megolm.v1.aes-sha2",
31+
"code": "m.unauthorised",
32+
"from_device": "BOB",
33+
"reason": "You are not authorised to read the message.",
34+
"room_id": "!room1:localhost",
35+
"sender_key": "[alice curve key]",
36+
"session_id": "lpRzTgD3Nook/Wk62Fm9ECWGnKYZgeCwO1Y+uuPJz/I"
37+
}
38+
]
39+
}

crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use serde_json::Value;
2222
use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKey};
2323

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

4042
/// The `m.forwarded_room_key` event content.
4143
///
42-
/// This is an enum over the different room key algorithms we support.
44+
/// This is an enum over the different room key algorithms we support. The
45+
/// currently-supported implementations are used to share
46+
/// [`InboundGroupSession`]s.
4347
///
4448
/// This event type is used to forward keys for end-to-end encryption.
45-
/// Typically it is encrypted as an m.room.encrypted event, then sent as a
49+
/// Typically, it is encrypted as an m.room.encrypted event, then sent as a
4650
/// to-device event.
51+
///
52+
/// See <https://spec.matrix.org/v1.13/client-server-api/#mforwarded_room_key>.
4753
#[derive(Debug, Deserialize)]
4854
#[serde(try_from = "RoomKeyHelper")]
4955
pub enum ForwardedRoomKeyContent {

crates/matrix-sdk-crypto/src/types/events/room_key.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use serde_json::{value::to_raw_value, Value};
2222
use vodozemac::megolm::SessionKey;
2323

2424
use super::{EventType, ToDeviceEvent};
25+
#[cfg(doc)]
26+
use crate::olm::InboundGroupSession;
2527
use crate::types::EventEncryptionAlgorithm;
2628

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

4143
/// The `m.room_key` event content.
4244
///
43-
/// This is an enum over the different room key algorithms we support.
45+
/// This is an enum over the different room key algorithms we support. The
46+
/// currently-supported implementations are used to share
47+
/// [`InboundGroupSession`]s.
4448
///
4549
/// This event type is used to exchange keys for end-to-end encryption.
46-
/// Typically it is encrypted as an m.room.encrypted event, then sent as a
50+
/// Typically, it is encrypted as an m.room.encrypted event, then sent as a
4751
/// to-device event.
52+
///
53+
/// See <https://spec.matrix.org/v1.13/client-server-api/#mroom_key>.
4854
#[derive(Debug, Deserialize)]
4955
#[serde(try_from = "RoomKeyHelper")]
5056
pub enum RoomKeyContent {

crates/matrix-sdk-crypto/src/types/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub mod events;
4949
mod one_time_keys;
5050
pub mod qr_login;
5151
pub mod requests;
52+
pub mod room_history;
5253

5354
pub use self::{backup::*, cross_signing::*, device_keys::*, one_time_keys::*};
5455
use crate::store::BackupDecryptionKey;

0 commit comments

Comments
 (0)