Skip to content

Commit 2dea4d1

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 2dea4d1

File tree

4 files changed

+330
-4
lines changed

4 files changed

+330
-4
lines changed

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

Lines changed: 188 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,36 @@ 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+
pub async fn build_room_key_bundle(
1999+
&self,
2000+
room_id: &RoomId,
2001+
) -> std::result::Result<RoomKeyBundle, CryptoStoreError> {
2002+
// TODO: make this WAY more efficient. We should only fetch sessions for the
2003+
// correct room.
2004+
let mut sessions = self.get_inbound_group_sessions().await?;
2005+
sessions.retain(|session| session.room_id == room_id);
2006+
2007+
let mut bundle = RoomKeyBundle::default();
2008+
for session in sessions {
2009+
if session.shared_history() {
2010+
bundle.room_keys.push(session.export().await.into());
2011+
} else {
2012+
bundle.withheld.push(RoomKeyWithheldContent::new(
2013+
session.algorithm().to_owned(),
2014+
WithheldCode::Unauthorised,
2015+
session.room_id().to_owned(),
2016+
session.session_id().to_owned(),
2017+
session.sender_key().to_owned(),
2018+
self.device_id().to_owned(),
2019+
));
2020+
}
2021+
}
2022+
2023+
Ok(bundle)
2024+
}
19902025
}
19912026

19922027
impl Deref for Store {
@@ -2021,12 +2056,17 @@ mod tests {
20212056
use std::pin::pin;
20222057

20232058
use futures_util::StreamExt;
2059+
use insta::assert_json_snapshot;
20242060
use matrix_sdk_test::async_test;
2025-
use ruma::{room_id, user_id};
2061+
use ruma::{device_id, room_id, user_id, DeviceId, RoomId, UserId};
2062+
use vodozemac::megolm::SessionKey;
20262063

20272064
use crate::{
2028-
machine::test_helpers::get_machine_pair, store::DehydratedDeviceKey,
2065+
machine::test_helpers::get_machine_pair,
2066+
olm::{InboundGroupSession, SenderData},
2067+
store::{DehydratedDeviceKey, MemoryStore},
20292068
types::EventEncryptionAlgorithm,
2069+
OlmMachine,
20302070
};
20312071

20322072
#[async_test]
@@ -2190,4 +2230,148 @@ mod tests {
21902230

21912231
assert!(pickle_key.is_err());
21922232
}
2233+
2234+
#[async_test]
2235+
async fn test_build_room_key_bundle() {
2236+
// Given: Alice has sent a number of room keys to Bob, including some in the
2237+
// wrong room, and some that are not marked as shared...
2238+
let alice = olm_machine_from_account_pickle(
2239+
user_id!("@a:s.co"),
2240+
device_id!("ALICE"),
2241+
olm_pickle_data1(),
2242+
)
2243+
.await;
2244+
2245+
let bob = olm_machine_from_account_pickle(
2246+
user_id!("@b:s.co"),
2247+
device_id!("BOB"),
2248+
olm_pickle_data2(),
2249+
)
2250+
.await;
2251+
2252+
let room1_id = room_id!("!room1:localhost");
2253+
let room2_id = room_id!("!room2:localhost");
2254+
2255+
/* base64-encoded Megolm session keys, created with:
2256+
2257+
println!(
2258+
"{}",
2259+
vodozemac::megolm::GroupSession::new(Default::default()).session_key().to_base64()
2260+
);
2261+
*/
2262+
2263+
let session_key1 = "AgAAAAC2XHVzsMBKs4QCRElJ92CJKyGtknCSC8HY7cQ7UYwndMKLQAejXLh5UA0l6s736mgctcUMNvELScUWrObdflrHo+vth/gWreXOaCnaSxmyjjKErQwyIYTkUfqbHy40RJfEesLwnN23on9XAkch/iy8R2+Jz7B8zfG01f2Ow2SxPQFnAndcO1ZSD2GmXgedy6n4B20MWI1jGP2wiexOWbFSya8DO/VxC9m5+/mF+WwYqdpKn9g4Y05Yw4uz7cdjTc3rXm7xK+8E7hI//5QD1nHPvuKYbjjM9u2JSL+Bzp61Cw";
2264+
let session_key2 = "AgAAAAC1BXreFTUQQSBGekTEuYxhdytRKyv4JgDGcG+VOBYdPNGgs807SdibCGJky4lJ3I+7ZDGHoUzZPZP/4ogGu4kxni0PWdtWuN7+5zsuamgoFF/BkaGeUUGv6kgIkx8pyPpM5SASTUEP9bN2loDSpUPYwfiIqz74DgC4WQ4435sTBctYvKz8n+TDJwdLXpyT6zKljuqADAioud+s/iqx9LYn9HpbBfezZcvbg67GtE113pLrvde3IcPI5s6dNHK2onGO2B2eoaobcen18bbEDnlUGPeIivArLya7Da6us14jBQ";
2265+
let session_key3 = "AgAAAAAM9KFsliaUUhGSXgwOzM5UemjkNH4n8NHgvC/y8hhw13zTF+ooGD4uIYEXYX630oNvQm/EvgZo+dkoc0re+vsqsx4sQeNODdSjcBsWOa0oDF+irQn9oYoLUDPI1IBtY1rX+FV99Zm/xnG7uFOX7aTVlko2GSdejy1w9mfobmfxu5aUc04A9zaKJP1pOthZvRAlhpymGYHgsDtWPrrjyc/yypMflE4kIUEEEtu1kT6mrAmcl615XYRAHYK9G2+fZsGvokwzbkl4nulGwcZMpQEoM0nD2o3GWgX81HW3nGfKBg";
2266+
let session_key4 = "AgAAAAA4Kkesxq2h4v9PLD6Sm3Smxspz1PXTqytQPCMQMkkrHNmzV2bHlJ+6/Al9cu8vh1Oj69AK0WUAeJOJuaiskEeg/PI3P03+UYLeC379RzgqwSHdBgdQ41G2vD6zpgmE/8vYToe+qpCZACtPOswZxyqxHH+T/Iq0nv13JmlFGIeA6fEPfr5Y28B49viG74Fs9rxV9EH5PfjbuPM/p+Sz5obShuaBPKQBX1jT913nEXPoIJ06exNZGr0285nw/LgVvNlmWmbqNnbzO2cNZjQWA+xZYz5FSfyCxwqEBbEdUCuRCQ";
2267+
let session1 = create_inbound_group_session_with_visibility(
2268+
&alice,
2269+
&room1_id,
2270+
&SessionKey::from_base64(session_key1).unwrap(),
2271+
true,
2272+
)
2273+
.await;
2274+
let session2 = create_inbound_group_session_with_visibility(
2275+
&alice,
2276+
&room1_id,
2277+
&SessionKey::from_base64(session_key2).unwrap(),
2278+
true,
2279+
)
2280+
.await;
2281+
2282+
let unshared_session = create_inbound_group_session_with_visibility(
2283+
&alice,
2284+
&room1_id,
2285+
&SessionKey::from_base64(session_key3).unwrap(),
2286+
false,
2287+
)
2288+
.await;
2289+
let room2_session = create_inbound_group_session_with_visibility(
2290+
&alice,
2291+
&room2_id,
2292+
&SessionKey::from_base64(session_key4).unwrap(),
2293+
true,
2294+
)
2295+
.await;
2296+
2297+
bob.store()
2298+
.save_inbound_group_sessions(&[
2299+
session1.clone(),
2300+
session2.clone(),
2301+
unshared_session,
2302+
room2_session,
2303+
])
2304+
.await
2305+
.unwrap();
2306+
2307+
// When I build the bundle
2308+
let mut bundle = bob.store().build_room_key_bundle(&room1_id).await.unwrap();
2309+
2310+
// Then the bundle matches the snapshot.
2311+
// We sort the sessions in the bundle, so that the snapshot is stable.
2312+
bundle.room_keys.sort_by_key(|session| session.session_id.clone());
2313+
assert_json_snapshot!(bundle);
2314+
}
2315+
2316+
/// Create a test [`OlmMachine`], backed by an in-memory store, based on the
2317+
/// given pickle data.
2318+
async fn olm_machine_from_account_pickle(
2319+
user_id: &UserId,
2320+
device_id: &DeviceId,
2321+
pickle: vodozemac::olm::AccountPickle,
2322+
) -> OlmMachine {
2323+
OlmMachine::with_store(user_id, device_id, MemoryStore::new(), Some(pickle.into()))
2324+
.await
2325+
.unwrap()
2326+
}
2327+
2328+
/// A hardcoded set of device keys, suitable for creating a test olm machine
2329+
/// with.
2330+
fn olm_pickle_data1() -> vodozemac::olm::AccountPickle {
2331+
let alice_account_pickle = serde_json::json!({
2332+
"signing_key": {"Normal": b"alicesigningkey12345678901234567"},
2333+
"diffie_hellman_key": b"alicediffiehelmankey123456789012",
2334+
"one_time_keys":{"next_key_id":0,"public_keys":{},"private_keys":{}},
2335+
"fallback_keys":{"key_id":0,"fallback_key":null,"previous_fallback_key":null}
2336+
});
2337+
2338+
serde_json::from_value(alice_account_pickle).unwrap()
2339+
}
2340+
2341+
/// Another set hardcoded set of device keys, suitable for creating a test
2342+
/// olm machine with.
2343+
fn olm_pickle_data2() -> vodozemac::olm::AccountPickle {
2344+
let alice_account_pickle = serde_json::json!({
2345+
"signing_key": {"Normal": b"alicesigningkey12345678901234567"},
2346+
"diffie_hellman_key": b"alicediffiehelmankey123456789012",
2347+
"one_time_keys":{"next_key_id":0,"public_keys":{},"private_keys":{}},
2348+
"fallback_keys":{"key_id":0,"fallback_key":null,"previous_fallback_key":null}
2349+
});
2350+
2351+
serde_json::from_value(alice_account_pickle).unwrap()
2352+
}
2353+
2354+
async fn create_inbound_group_session_with_visibility(
2355+
olm_machine: &OlmMachine,
2356+
room_id: &RoomId,
2357+
session_key: &SessionKey,
2358+
shared_history: bool,
2359+
) -> InboundGroupSession {
2360+
let static_account = olm_machine.store().static_account();
2361+
let sender_key = static_account.identity_keys.curve25519;
2362+
let signing_key = static_account.identity_keys.ed25519;
2363+
2364+
let session = InboundGroupSession::new(
2365+
sender_key,
2366+
signing_key,
2367+
room_id,
2368+
session_key,
2369+
SenderData::unknown(),
2370+
EventEncryptionAlgorithm::MegolmV1AesSha2,
2371+
None,
2372+
shared_history,
2373+
)
2374+
.unwrap();
2375+
session
2376+
}
21932377
}
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": "oW3g1uEcvDKEPNpyIfbAj2zz4U+qgO2jIWHgmNxAlyQ",
11+
"session_id": "AWcCd1w7VlIPYaZeB53LqfgHbQxYjWMY/bCJ7E5ZsVI",
12+
"session_key": "AQAAAAC2XHVzsMBKs4QCRElJ92CJKyGtknCSC8HY7cQ7UYwndMKLQAejXLh5UA0l6s736mgctcUMNvELScUWrObdflrHo+vth/gWreXOaCnaSxmyjjKErQwyIYTkUfqbHy40RJfEesLwnN23on9XAkch/iy8R2+Jz7B8zfG01f2Ow2SxPQFnAndcO1ZSD2GmXgedy6n4B20MWI1jGP2wiexOWbFS",
13+
"sender_claimed_keys": {
14+
"ed25519": "o45ljUqAb374+5wsaAcX2XBrSZQt0Sr2XnxnJvaBWEc"
15+
}
16+
},
17+
{
18+
"algorithm": "m.megolm.v1.aes-sha2",
19+
"room_id": "!room1:localhost",
20+
"sender_key": "oW3g1uEcvDKEPNpyIfbAj2zz4U+qgO2jIWHgmNxAlyQ",
21+
"session_id": "y1i8rPyf5MMnB0tenJPrMqWO6oAMCKi536z+KrH0tic",
22+
"session_key": "AQAAAAC1BXreFTUQQSBGekTEuYxhdytRKyv4JgDGcG+VOBYdPNGgs807SdibCGJky4lJ3I+7ZDGHoUzZPZP/4ogGu4kxni0PWdtWuN7+5zsuamgoFF/BkaGeUUGv6kgIkx8pyPpM5SASTUEP9bN2loDSpUPYwfiIqz74DgC4WQ4435sTBctYvKz8n+TDJwdLXpyT6zKljuqADAioud+s/iqx9LYn",
23+
"sender_claimed_keys": {
24+
"ed25519": "o45ljUqAb374+5wsaAcX2XBrSZQt0Sr2XnxnJvaBWEc"
25+
}
26+
}
27+
],
28+
"withheld": [
29+
{
30+
"algorithm": "m.megolm.v1.aes-sha2",
31+
"reason": "You are not authorised to read the message.",
32+
"code": "m.unauthorised",
33+
"from_device": "BOB",
34+
"room_id": "!room1:localhost",
35+
"sender_key": "oW3g1uEcvDKEPNpyIfbAj2zz4U+qgO2jIWHgmNxAlyQ",
36+
"session_id": "lpRzTgD3Nook/Wk62Fm9ECWGnKYZgeCwO1Y+uuPJz/I"
37+
}
38+
]
39+
}

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;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2025 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
//! Types for sharing encrypted room history, per MSC4268
18+
19+
use std::fmt::Debug;
20+
21+
use ruma::{DeviceKeyAlgorithm, OwnedRoomId};
22+
use serde::{Deserialize, Serialize};
23+
use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey};
24+
25+
#[cfg(doc)]
26+
use crate::olm::InboundGroupSession;
27+
use crate::{
28+
olm::ExportedRoomKey,
29+
types::{
30+
deserialize_curve_key, events::room_key_withheld::RoomKeyWithheldContent,
31+
serialize_curve_key, EventEncryptionAlgorithm, SigningKeys,
32+
},
33+
};
34+
35+
/// A bundle of room keys, for sharing encrypted room history.
36+
#[derive(Deserialize, Serialize, Debug, Default)]
37+
pub struct RoomKeyBundle {
38+
/// Keys that we are sharing with the recipient.
39+
pub room_keys: Vec<SharedRoomKey>,
40+
41+
/// Keys that we are *not* sharing with the recipient.
42+
pub withheld: Vec<RoomKeyWithheldContent>,
43+
}
44+
45+
/// An [`InboundGroupSession`] for sharing as part of the key bundle.
46+
#[derive(Deserialize, Serialize)]
47+
pub struct SharedRoomKey {
48+
/// The encryption algorithm that the session uses.
49+
pub algorithm: EventEncryptionAlgorithm,
50+
51+
/// The room where the session is used.
52+
pub room_id: OwnedRoomId,
53+
54+
/// The Curve25519 key of the device which initiated the session originally.
55+
#[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
56+
pub sender_key: Curve25519PublicKey,
57+
58+
/// The ID of the session that the key is for.
59+
pub session_id: String,
60+
61+
/// The key for the session.
62+
pub session_key: ExportedSessionKey,
63+
64+
/// The Ed25519 key of the device which initiated the session originally.
65+
#[serde(default)]
66+
pub sender_claimed_keys: SigningKeys<DeviceKeyAlgorithm>,
67+
}
68+
69+
impl Debug for SharedRoomKey {
70+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71+
f.debug_struct("SharedRoomKey")
72+
.field("algorithm", &self.algorithm)
73+
.field("room_id", &self.room_id)
74+
.field("sender_key", &self.sender_key)
75+
.field("session_id", &self.session_id)
76+
.field("sender_claimed_keys", &self.sender_claimed_keys)
77+
.finish_non_exhaustive()
78+
}
79+
}
80+
81+
impl From<ExportedRoomKey> for SharedRoomKey {
82+
fn from(exported_room_key: ExportedRoomKey) -> Self {
83+
let ExportedRoomKey {
84+
algorithm,
85+
room_id,
86+
sender_key,
87+
session_id,
88+
session_key,
89+
sender_claimed_keys,
90+
shared_history: _,
91+
forwarding_curve25519_key_chain: _,
92+
} = exported_room_key;
93+
SharedRoomKey {
94+
algorithm,
95+
room_id,
96+
sender_key,
97+
session_id,
98+
session_key,
99+
sender_claimed_keys,
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)