Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
aa63ada
feat(crypto): Add OutboundGroupSession::encrypt_state
kaylendog Aug 6, 2025
2173e36
feat(crypto): Add GroupSessionManager::encrypt_state
kaylendog Aug 6, 2025
c5fabf5
feat(crypto): Add state event encryption methods to OlmMachine
kaylendog Aug 6, 2025
3192d17
feat(base): Add EncryptionState::StateEncrypted
kaylendog Aug 6, 2025
bbf047d
feat(sdk): Add SendStateEventRaw
kaylendog Aug 6, 2025
7e2c076
feat(sdk): Implement IntoFuture for SendStateEventRaw
kaylendog Aug 6, 2025
bde0ef1
feat(sdk): Modify Room::send_state_event_raw to return SendStateEventRaw
kaylendog Aug 6, 2025
9ef0419
feat(sdk): Add SendStateEvent
kaylendog Aug 6, 2025
1eef067
feat(sdk): Implement IntoFuture for SendStateEvent
kaylendog Aug 6, 2025
d63f0ee
feat(sdk): Use SendStateEvent future in Room send_state methods
kaylendog Aug 6, 2025
89b367f
feat(sdk): Add Room::enable_encryption_with_state
kaylendog Aug 6, 2025
cd56ffc
feat(base): Add sync reponse processor for AnySyncState::RoomEncrypted
kaylendog Aug 6, 2025
7023b68
feat(sdk): Add assert_let_decrypted_state_event_content test utility …
kaylendog Aug 6, 2025
53d3a2e
feat(sdk): Attempt state event decryption in Room::try_decrypt_event
kaylendog Aug 6, 2025
a51bec6
test(e2ee): Add integration test for encrypted state events
kaylendog Aug 6, 2025
61d40bd
feat(crypto): Add RoomSettings::enable_encrypted_state_events (WASM SDK)
kaylendog Aug 7, 2025
d6e15b6
refactor(crypto): De-duplicate OutboundGroupSession::encrypt and ::en…
kaylendog Aug 8, 2025
b707517
refactor(sdk): De-dupe room::enable_encryption and ...with_state to h…
kaylendog Aug 8, 2025
a8b7d11
refactor(sdk): De-dupe SendRawMessageLikeEvent and SendRawStateEvent …
kaylendog Aug 8, 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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "e73f302e4df7f5f0511fca1aa4
"compat-lax-room-create-deser",
"compat-lax-room-topic-deser",
"unstable-msc3401",
"unstable-msc3414",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ pub async fn update_any_room(
ambiguity_cache,
&mut new_user_ids,
state_store,
#[cfg(feature = "e2e-encryption")]
e2ee.clone(),
)
.await?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ pub async fn update_joined_room(
ambiguity_cache,
&mut new_user_ids,
state_store,
#[cfg(feature = "e2e-encryption")]
e2ee.clone(),
)
.await?;

Expand Down Expand Up @@ -180,6 +182,8 @@ pub async fn update_left_room(
ambiguity_cache,
&mut (),
state_store,
#[cfg(feature = "e2e-encryption")]
e2ee.clone(),
)
.await?;

Expand Down
77 changes: 77 additions & 0 deletions crates/matrix-sdk-base/src/response_processors/state_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use serde::Deserialize;
use tracing::warn;

use super::Context;
#[cfg(feature = "e2e-encryption")]
use super::e2ee;
use crate::store::BaseStateStore;

/// Collect [`AnySyncStateEvent`].
Expand Down Expand Up @@ -89,6 +91,7 @@ pub mod sync {
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
state_store: &BaseStateStore,
#[cfg(feature = "e2e-encryption")] e2ee: super::e2ee::E2EE<'_>,
) -> StoreResult<()>
where
U: NewUsers,
Expand Down Expand Up @@ -142,6 +145,80 @@ pub mod sync {
}
}

#[cfg(feature = "e2e-encryption")]
AnySyncStateEvent::RoomEncrypted(outer) => {
use matrix_sdk_crypto::RoomEventDecryptionResult;
use tracing::{debug, warn};

debug!(event_id = ?outer.event_id(), "Received encrypted state event, attempting decryption...");

let Some(olm_machine) = e2ee.olm_machine else {
panic!();
};

let decrypted_event = olm_machine
.try_decrypt_room_event(
raw_event.cast_ref_unchecked(),
&room_info.room_id,
e2ee.decryption_settings,
)
.await
.expect("OlmMachine was not started");

// Skip state events that failed to decrypt.
let RoomEventDecryptionResult::Decrypted(room_event) = decrypted_event else {
warn!(event_id = ?outer.event_id(), "Failed to decrypt state event");
continue;
};

// Unpack event type and state key from outer, or discard if this fails.
let Some((outer_event_type, outer_state_key)) =
outer.state_key().split_once(":")
else {
warn!(
event_id = outer.event_id().as_str(),
state_key = event.state_key(),
"Malformed state key"
);
continue;
};

let inner =
match room_event.event.deserialize_as_unchecked::<AnySyncStateEvent>() {
Ok(inner) => inner,
Err(e) => {
warn!("Malformed event body: {e}");
continue;
}
};

// Check event types match, discard if not.
let inner_event_type = inner.event_type().to_string();
if outer_event_type != inner_event_type {
warn!(
event_id = outer.event_id().as_str(),
expected = outer_event_type,
found = inner_event_type,
"Mismatched event type"
);
continue;
}

// Check state keys match, discard if not.
if outer_state_key != inner.state_key() {
warn!(
event_id = outer.event_id().as_str(),
expected = outer_state_key,
found = inner.state_key(),
"Mismatched state key"
);
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

This functionality belongs in the crypto crate: it is not specific to /sync, but should happen whereever we decrypt a state event.

It seems like we we need an olm_machine.decrypt_state_event which does these checks somehow. Not sure exactly what the types for that look like off the top of my head: see if you can figure something out, if not give me a ping to see if we can figure something out together.


debug!(event_id = ?outer.event_id(), "Decrypted state event successfully.");
room_info.handle_state_event(&inner);
Comment on lines +150 to +219
Copy link
Member

Choose a reason for hiding this comment

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

this looks a bit complicated to be in the middle of a match statement. Pull it out to a separate function, please.

}

_ => {
room_info.handle_state_event(event);
}
Expand Down
15 changes: 13 additions & 2 deletions crates/matrix-sdk-base/src/room/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ pub enum EncryptionState {
/// The room is encrypted.
Encrypted,

/// The room is encrypted, additionally requiring state events to be
/// encrypted.
StateEncrypted,

/// The room is not encrypted.
NotEncrypted,

Expand All @@ -45,9 +49,16 @@ pub enum EncryptionState {
}

impl EncryptionState {
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted] or
/// [`StateEncrypted`][Self::StateEncrypted].
pub fn is_encrypted(&self) -> bool {
matches!(self, Self::Encrypted)
matches!(self, Self::Encrypted | Self::StateEncrypted)
}

/// Check whether `EncryptionState` is
/// [`StateEncrypted`][Self::StateEncrypted].
pub fn is_state_encrypted(&self) -> bool {
matches!(self, Self::StateEncrypted)
}

/// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
Expand Down
14 changes: 11 additions & 3 deletions crates/matrix-sdk-base/src/room/room_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,10 +619,18 @@ impl RoomInfo {
pub fn encryption_state(&self) -> EncryptionState {
if !self.encryption_state_synced {
EncryptionState::Unknown
} else if self.base_info.encryption.is_some() {
EncryptionState::Encrypted
} else {
EncryptionState::NotEncrypted
self.base_info
.encryption
.as_ref()
.map(|state| {
if state.encrypt_state_events {
EncryptionState::StateEncrypted
} else {
EncryptionState::Encrypted
}
})
.unwrap_or(EncryptionState::NotEncrypted)
}
}

Expand Down
60 changes: 59 additions & 1 deletion crates/matrix-sdk-crypto/src/machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
time::Duration,
Expand Down Expand Up @@ -44,8 +45,9 @@ use ruma::{
},
assign,
events::{
room::encrypted::unstable_state::StateRoomEncryptedEventContent,
secret::request::SecretName, AnyMessageLikeEvent, AnyMessageLikeEventContent,
AnyToDeviceEvent, MessageLikeEventContent,
AnyStateEventContent, AnyToDeviceEvent, MessageLikeEventContent, StateEventContent,
},
serde::{JsonObject, Raw},
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId,
Expand Down Expand Up @@ -1100,6 +1102,62 @@ impl OlmMachine {
self.inner.group_session_manager.encrypt(room_id, event_type, content).await
}

/// Encrypt a state event for the given room.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the event should be
/// encrypted.
///
/// * `content` - The plaintext content of the event that should be
/// encrypted.
///
/// * `state_key` - The associated state key of the event.
pub async fn encrypt_state_event<C, K>(
&self,
room_id: &RoomId,
content: C,
state_key: K,
) -> MegolmResult<Raw<StateRoomEncryptedEventContent>>
where
C: StateEventContent,
C::StateKey: Borrow<K>,
K: AsRef<str>,
{
let event_type = content.event_type().to_string();
let content = Raw::new(&content)?.cast_unchecked();
self.encrypt_state_event_raw(room_id, &event_type, state_key.as_ref(), &content).await
}

/// Encrypt a state event for the given state event using its raw JSON
/// content and state key.
///
/// This method is equivalent to [`OlmMachine::encrypt_state_event`]
/// method but operates on an arbitrary JSON value instead of strongly-typed
/// event content struct.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the message should be
/// encrypted.
///
/// * `content` - The plaintext content of the event that should be
/// encrypted as a raw JSON value.
///
/// * `state_key` - The associated state key of the event.
Comment on lines +1141 to +1147
Copy link
Member

Choose a reason for hiding this comment

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

event_type is missing here, and the order is inconsistent with the actual arguments

pub async fn encrypt_state_event_raw(
&self,
room_id: &RoomId,
event_type: &str,
state_key: &str,
content: &Raw<AnyStateEventContent>,
) -> MegolmResult<Raw<StateRoomEncryptedEventContent>> {
self.inner
.group_session_manager
.encrypt_state(room_id, event_type, state_key, content)
.await
}

/// Forces the currently active room key, which is used to encrypt messages,
/// to be rotated.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async fn test_stores_and_returns_room_settings() {

let settings = RoomSettings {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
encrypt_state_events: false,
only_allow_trusted_devices: true,
session_rotation_period: Some(Duration::from_secs(10)),
session_rotation_period_messages: Some(1234),
Expand Down
76 changes: 69 additions & 7 deletions crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ use std::{
use matrix_sdk_common::{deserialized_responses::WithheldCode, locks::RwLock as StdRwLock};
use ruma::{
events::{
room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
AnyMessageLikeEventContent,
room::{
encrypted::unstable_state::StateRoomEncryptedEventContent,
encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility,
},
AnyMessageLikeEventContent, AnyStateEventContent,
},
serde::Raw,
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId,
Expand Down Expand Up @@ -488,14 +491,75 @@ impl OutboundGroupSession {
}

let payload = Payload { event_type, content, room_id: &self.room_id };
let payload_json =
serde_json::to_string(&payload).expect("payload serialization never fails");

let relates_to = content
.get_field::<serde_json::Value>("m.relates_to")
.expect("serde_json::Value deserialization with valid JSON input never fails");

let ciphertext = self.encrypt_helper(payload_json).await;
self.encrypt_inner(&payload, relates_to).await
}

/// Encrypt a room state event for the given room.
///
/// Beware that a room key needs to be shared before this method
/// can be called using the `share_room_key()` method.
///
/// # Arguments
///
/// * `event_type` - The plaintext type of the event, the outer type of the
/// event will become `m.room.encrypted`.
///
/// * `state_key` - The plaintext state key of the event, the outer state
/// key will be derived from this and the event type.
///
/// * `content` - The plaintext content of the message that should be
/// encrypted in raw JSON form.
///
/// # Panics
///
/// Panics if the content can't be serialized.
pub async fn encrypt_state(
&self,
event_type: &str,
state_key: &str,
content: &Raw<AnyStateEventContent>,
) -> Raw<StateRoomEncryptedEventContent> {
#[derive(Serialize)]
struct Payload<'a> {
#[serde(rename = "type")]
event_type: &'a str,
state_key: &'a str,
content: &'a Raw<AnyStateEventContent>,
room_id: &'a RoomId,
}

let payload = Payload { event_type, state_key, content, room_id: &self.room_id };
self.encrypt_inner(&payload, None).await.cast_unchecked()
}

/// Encrypt an arbitrary event for the given room.
///
/// Beware that a room key needs to be shared before this method
/// can be called using the `share_room_key()` method.
///
/// # Arguments
///
/// * `payload` - The plaintext content of the event that should be
/// encrypted in raw JSON form.
///
/// # Panics
///
/// Panics if the content can't be serialized.
async fn encrypt_inner<T: Serialize>(
&self,
payload: &T,
relates_to: Option<serde_json::Value>,
) -> Raw<RoomEncryptedEventContent> {
let ciphertext = self
.encrypt_helper(
serde_json::to_string(payload).expect("payload serialization never fails"),
)
.await;
let scheme: RoomEventEncryptionScheme = match self.settings.algorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2 => MegolmV1AesSha2Content {
ciphertext,
Expand All @@ -513,9 +577,7 @@ impl OutboundGroupSession {
"An outbound group session is always using one of the supported algorithms"
),
};

let content = RoomEncryptedEventContent { scheme, relates_to, other: Default::default() };

Raw::new(&content).expect("m.room.encrypted event content can always be serialized")
}

Expand Down
Loading
Loading