Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5d9fdd8
feat: Add `RoomKeyWithheldEntry` to wrap to-device and bundle payloads.
kaylendog Oct 1, 2025
e635219
feat: Append withheld info from room key bundle to store.
kaylendog Oct 1, 2025
7ef47d2
fix: Clippy lints, copy over clone, `to_owned`.
kaylendog Oct 1, 2025
a98f852
docs: Update CHANGELOGs.
kaylendog Oct 1, 2025
20246ba
feat(crypto): Store `sender` in `RoomKeyWithheldEntry::Bundle`.
kaylendog Oct 2, 2025
fd3943c
feat: Use struct for `RoomKeyWithheldEntry`, move to store.
kaylendog Oct 2, 2025
e10a1e6
tests: Test deserializing `m.room_key.withheld` to withheld entry.
kaylendog Oct 2, 2025
55c005d
tests: Use `serde_json::json!()` in test over invoking `Serialize`.
kaylendog Oct 3, 2025
cc74a92
fix(crypto): Re-restrict `OlmMachine::add_withheld_info` visibility.
kaylendog Oct 6, 2025
c7cbca5
docs(crypto): Clarify doc comments for RoomKeyWithheldEntry.
kaylendog Oct 6, 2025
a581fdd
refactor(cryptor): Split `receive_room_key_bundle` to helper methods.
kaylendog Oct 6, 2025
948402f
test(crypto): Add fix for `experimental-algorithms` feature.
kaylendog Oct 6, 2025
88e0dfb
test(crypto): Redact algorithm field in room key bundle snapshot.
kaylendog Oct 6, 2025
0d56345
feat(crypto): Add all withheld types (with room ID) to store.
kaylendog Oct 7, 2025
869007e
docs(crypto): Point URLs to correct MSC.
kaylendog Oct 9, 2025
48f87a4
docs(crypto): Clarify session algorithm dependency on feature flag.
kaylendog Oct 9, 2025
1510108
feat(crypto): Explicit `RoomKeyWithheldContent::Unknown` match case.
kaylendog Oct 9, 2025
741505d
fix(crypto): Remove unnecessary inverted feature flag.
kaylendog Oct 9, 2025
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
10 changes: 10 additions & 0 deletions crates/matrix-sdk-crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased] - ReleaseDate

### Features

- Improves feedback support for shared history when downloading room key bundles.
([#5737](https://github.com/matrix-org/matrix-rust-sdk/pull/5737))
- Add `RoomKeyWithheldEntry` enum, wrapping either a received to-device `m.room_key.withheld` event or
its content, if derived from a downloaded room key bundle.
- `OlmMachine::receive_room_key_bundle` now appends withheld key information to the store.
- [**breaking**] `Changes::withheld_session_info` now stores a `RookKeyWithheldEntry` in each `room-id`-`session-id` entry.
- [**breaking**] `CryptoStore::get_withheld_info` now returns `Result<Option<RookKeyWithheldEntry>>`. This change also affects `MemoryStore`.

### Bug Fixes

- Fix a bug introduced in 0.14.0 which meant that the serialization of the value returned by `OtherUserIdentity::verification_request_content` did not include a `msgtype` field.
Expand Down
8 changes: 4 additions & 4 deletions crates/matrix-sdk-crypto/src/machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ impl OlmMachine {
Ok(())
}

fn add_withheld_info(&self, changes: &mut Changes, event: &RoomKeyWithheldEvent) {
pub(crate) fn add_withheld_info(&self, changes: &mut Changes, event: &RoomKeyWithheldEvent) {
debug!(?event.content, "Processing `m.room_key.withheld` event");

if let RoomKeyWithheldContent::MegolmV1AesSha2(
Expand All @@ -1010,7 +1010,7 @@ impl OlmMachine {
.withheld_session_info
.entry(c.room_id.to_owned())
.or_default()
.insert(c.session_id.to_owned(), event.to_owned());
.insert(c.session_id.to_owned(), event.to_owned().into());
}
}

Expand Down Expand Up @@ -2029,7 +2029,7 @@ impl OlmMachine {
.store
.get_withheld_info(room_id, content.session_id())
.await?
.map(|e| e.content.withheld_code());
.map(|e| e.content().withheld_code());

if withheld_code.is_some() {
// Partially withheld, report with a withheld code if we have one.
Expand Down Expand Up @@ -2145,7 +2145,7 @@ impl OlmMachine {
.store
.get_withheld_info(room_id, session_id)
.await?
.map(|e| e.content.withheld_code());
.map(|e| e.content().withheld_code());
Err(MegolmError::MissingRoomKey(withheld_code))
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-crypto/src/machine/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ async fn test_withheld_unverified() {

assert_eq!(&withheld_received[0].room_id, room_id);
assert_matches!(
&withheld_received[0].withheld_event.content,
&withheld_received[0].withheld_event.content(),
RoomKeyWithheldContent::MegolmV1AesSha2(MegolmV1AesSha2WithheldContent::Unverified(
unverified_withheld_content
))
Expand Down
15 changes: 8 additions & 7 deletions crates/matrix-sdk-crypto/src/store/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ macro_rules! cryptostore_integration_tests {
room_key_withheld::{
CommonWithheldCodeContent, MegolmV1AesSha2WithheldContent,
RoomKeyWithheldContent,
RoomKeyWithheldEntry,
},
room_key_bundle::RoomKeyBundleContent,
secret_send::SecretSendContent,
Expand Down Expand Up @@ -1138,7 +1139,7 @@ macro_rules! cryptostore_integration_tests {
async fn test_withheld_info_storage() {
let (account, store) = get_loaded_store("withheld_info_storage").await;

let mut info_list: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
let mut info_list: BTreeMap<_, BTreeMap<_, RoomKeyWithheldEntry>> = BTreeMap::new();

let user_id = account.user_id().to_owned();
let room_id = room_id!("!DwLygpkclUAfQNnfva:example.com");
Expand All @@ -1163,7 +1164,7 @@ macro_rules! cryptostore_integration_tests {
info_list
.entry(room_id.to_owned())
.or_default()
.insert(session_id_1.to_owned(), event);
.insert(session_id_1.to_owned(), event.into());

let content = RoomKeyWithheldContent::MegolmV1AesSha2(
MegolmV1AesSha2WithheldContent::BlackListed(
Expand All @@ -1183,7 +1184,7 @@ macro_rules! cryptostore_integration_tests {
info_list
.entry(room_id.to_owned())
.or_default()
.insert(session_id_2.to_owned(), event);
.insert(session_id_2.to_owned(), event.into());

let changes = Changes { withheld_session_info: info_list, ..Default::default() };
store.save_changes(changes).await.unwrap();
Expand All @@ -1192,16 +1193,16 @@ macro_rules! cryptostore_integration_tests {

assert_matches!(
is_withheld, Some(event)
if event.content.algorithm() == EventEncryptionAlgorithm::MegolmV1AesSha2 &&
event.content.withheld_code() == WithheldCode::Unverified
if event.content().algorithm() == EventEncryptionAlgorithm::MegolmV1AesSha2 &&
event.content().withheld_code() == WithheldCode::Unverified
);

let is_withheld = store.get_withheld_info(room_id, session_id_2).await.unwrap();

assert_matches!(
is_withheld, Some(event)
if event.content.algorithm() == EventEncryptionAlgorithm::MegolmV1AesSha2 &&
event.content.withheld_code() == WithheldCode::Blacklisted
if event.content().algorithm() == EventEncryptionAlgorithm::MegolmV1AesSha2 &&
event.content().withheld_code() == WithheldCode::Blacklisted
);

let other_room_id = room_id!("!nQRyiRFuyUhXeaQfiR:example.com");
Expand Down
10 changes: 5 additions & 5 deletions crates/matrix-sdk-crypto/src/store/memorystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use crate::{
OutboundGroupSession, PickledAccount, PickledInboundGroupSession, PickledSession,
PrivateCrossSigningIdentity, SenderDataType, StaticAccountData,
},
types::events::room_key_withheld::RoomKeyWithheldEvent,
types::events::room_key_withheld::RoomKeyWithheldEntry,
};

fn encode_key_info(info: &SecretInfo) -> String {
Expand Down Expand Up @@ -97,7 +97,7 @@ pub struct MemoryStore {
identities: StdRwLock<HashMap<OwnedUserId, String>>,
outgoing_key_requests: StdRwLock<HashMap<OwnedTransactionId, GossipRequest>>,
key_requests_by_info: StdRwLock<HashMap<String, OwnedTransactionId>>,
direct_withheld_info: StdRwLock<HashMap<OwnedRoomId, HashMap<String, RoomKeyWithheldEvent>>>,
direct_withheld_info: StdRwLock<HashMap<OwnedRoomId, HashMap<String, RoomKeyWithheldEntry>>>,
custom_values: StdRwLock<HashMap<String, Vec<u8>>>,
leases: StdRwLock<HashMap<String, (String, Instant)>>,
secret_inbox: StdRwLock<HashMap<String, Vec<GossippedSecret>>>,
Expand Down Expand Up @@ -420,7 +420,7 @@ impl CryptoStore for MemoryStore {
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<Option<RoomKeyWithheldEvent>> {
) -> Result<Option<RoomKeyWithheldEntry>> {
Ok(self
.direct_withheld_info
.read()
Expand Down Expand Up @@ -1276,7 +1276,7 @@ mod integration_tests {
},
CryptoStore,
},
types::events::room_key_withheld::RoomKeyWithheldEvent,
types::events::room_key_withheld::RoomKeyWithheldEntry,
Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, Session, UserIdentityData,
};

Expand Down Expand Up @@ -1372,7 +1372,7 @@ mod integration_tests {
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<Option<RoomKeyWithheldEvent>, Self::Error> {
) -> Result<Option<RoomKeyWithheldEntry>, Self::Error> {
self.0.get_withheld_info(room_id, session_id).await
}

Expand Down
134 changes: 128 additions & 6 deletions crates/matrix-sdk-crypto/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ use crate::{
Session, StaticAccountData,
},
types::{
events::room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldEntry},
BackupSecrets, CrossSigningSecrets, MegolmBackupV1Curve25519AesSha2Secrets, RoomKeyExport,
SecretsBundle,
},
Expand Down Expand Up @@ -1632,12 +1633,11 @@ impl Store {

tracing::Span::current().record("sender_data", tracing::field::debug(&sender_data));

match sender_data {
match &sender_data {
SenderData::UnknownDevice { .. }
| SenderData::VerificationViolation(_)
| SenderData::DeviceInfo { .. } => {
warn!("Not accepting a historic room key bundle due to insufficient trust in the sender");
Ok(())
}
SenderData::SenderUnverified(_) | SenderData::SenderVerified(_) => {
let (good, bad): (Vec<_>, Vec<_>) = bundle.room_keys.iter().partition_map(|key| {
Expand Down Expand Up @@ -1681,10 +1681,30 @@ impl Store {
self.import_sessions_impl(good, None, progress_listener).await?;
}
}
}
}

Ok(())
let mut changes = Changes::default();
for withheld in &bundle.withheld {
if let RoomKeyWithheldContent::MegolmV1AesSha2(
MegolmV1AesSha2WithheldContent::BlackListed(c)
| MegolmV1AesSha2WithheldContent::Unverified(c)
| MegolmV1AesSha2WithheldContent::Unauthorised(c)
| MegolmV1AesSha2WithheldContent::Unavailable(c),
) = withheld
{
changes.withheld_session_info.entry(c.room_id.to_owned()).or_default().insert(
c.session_id.to_owned(),
RoomKeyWithheldEntry::Bundle {
sender: bundle_info.sender_user.clone(),
content: withheld.to_owned(),
},
);
}
}
self.save_changes(changes).await?;

Ok(())
}
}

Expand Down Expand Up @@ -1717,17 +1737,32 @@ impl matrix_sdk_common::cross_process_lock::TryLock for LockableCryptoStore {
mod tests {
use std::pin::pin;

use assert_matches2::assert_matches;
use futures_util::StreamExt;
use insta::{_macro_support::Content, assert_json_snapshot, internals::ContentPath};
use matrix_sdk_test::async_test;
use ruma::{device_id, room_id, user_id, RoomId};
use ruma::{
device_id,
events::room::{EncryptedFileInit, JsonWebKeyInit},
owned_mxc_uri, room_id,
serde::Base64,
user_id, RoomId,
};
use vodozemac::megolm::SessionKey;

use crate::{
machine::test_helpers::get_machine_pair,
olm::{InboundGroupSession, SenderData},
store::types::DehydratedDeviceKey,
types::EventEncryptionAlgorithm,
store::types::{DehydratedDeviceKey, StoredRoomKeyBundleData},
types::{
events::{
room_key_bundle::RoomKeyBundleContent,
room_key_withheld::{
MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, RoomKeyWithheldEntry,
},
},
EventEncryptionAlgorithm,
},
OlmMachine,
};

Expand Down Expand Up @@ -1969,6 +2004,93 @@ mod tests {
});
}

#[async_test]
async fn test_receive_room_key_bundle() {
let alice = OlmMachine::new(user_id!("@a:s.co"), device_id!("ALICE")).await;
let alice_key = alice.identity_keys().curve25519;
let bob = OlmMachine::new(user_id!("@b:s.co"), device_id!("BOB")).await;

let room_id = room_id!("!room1:localhost");

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 sessions = [
create_inbound_group_session_with_visibility(
&alice,
room_id,
&SessionKey::from_base64(session_key1).unwrap(),
true,
),
create_inbound_group_session_with_visibility(
&alice,
room_id,
&SessionKey::from_base64(session_key2).unwrap(),
false,
),
];

alice.store().save_inbound_group_sessions(&sessions).await.unwrap();
let bundle = alice.store().build_room_key_bundle(room_id).await.unwrap();

bob.store()
.receive_room_key_bundle(
&StoredRoomKeyBundleData {
sender_user: alice.user_id().to_owned(),
sender_key: alice_key,
sender_data: SenderData::sender_verified(
alice.user_id(),
device_id!("ALICE"),
alice.identity_keys().ed25519,
),

bundle_data: RoomKeyBundleContent {
room_id: room_id.to_owned(),
// This isn't used at all in the method call, so we can fill it with
// garbage.
file: EncryptedFileInit {
url: owned_mxc_uri!("mxc://example.com/0"),
key: JsonWebKeyInit {
kty: "oct".to_owned(),
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
alg: "A256CTR.".to_owned(),
k: Base64::new(vec![0u8; 128]),
ext: true,
}
.into(),
iv: Base64::new(vec![0u8; 128]),
hashes: vec![("sha256".to_owned(), Base64::new(vec![0u8; 128]))]
.into_iter()
.collect(),
v: "v2".to_owned(),
}
.into(),
},
},
bundle,
|_, _| {},
)
.await
.unwrap();

// The room key should be imported successfully
let imported_sessions =
bob.store().get_inbound_group_sessions_by_room_id(room_id).await.unwrap();

assert_eq!(imported_sessions.len(), 1);
assert_eq!(imported_sessions[0].room_id(), room_id);

assert_matches!(
bob.store().get_withheld_info(room_id, sessions[1].session_id()).await.unwrap(),
Some(RoomKeyWithheldEntry::Bundle {
content: RoomKeyWithheldContent::MegolmV1AesSha2(
MegolmV1AesSha2WithheldContent::Unauthorised(_)
),
..
})
);
}

/// Create an inbound Megolm session for the given room.
///
/// `olm_machine` is used to set the `sender_key` and `signing_key`
Expand Down
6 changes: 3 additions & 3 deletions crates/matrix-sdk-crypto/src/store/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::{
InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity,
SenderDataType, Session,
},
types::events::room_key_withheld::RoomKeyWithheldEvent,
types::events::room_key_withheld::RoomKeyWithheldEntry,
Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, UserIdentityData,
};

Expand Down Expand Up @@ -116,7 +116,7 @@ pub trait CryptoStore: AsyncTraitDeps {
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<Option<RoomKeyWithheldEvent>, Self::Error>;
) -> Result<Option<RoomKeyWithheldEntry>, Self::Error>;

/// Get all the inbound group sessions we have stored.
async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>, Self::Error>;
Expand Down Expand Up @@ -588,7 +588,7 @@ impl<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<Option<RoomKeyWithheldEvent>, Self::Error> {
) -> Result<Option<RoomKeyWithheldEntry>, Self::Error> {
self.0.get_withheld_info(room_id, session_id).await.map_err(Into::into)
}

Expand Down
Loading
Loading