Skip to content
Closed
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
5 changes: 5 additions & 0 deletions crates/matrix-sdk-common/src/deserialized_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,11 @@ pub enum UnableToDecryptReason {
/// cross-signing identity did not satisfy the requested
/// `TrustRequirement`.
SenderIdentityNotTrusted(VerificationLevel),

/// The outer state key could not be verified against the inner encrypted
/// state key and type.
#[cfg(feature = "experimental-encrypted-state-events")]
StateKeyVerificationFailed,
Comment on lines +1001 to +1005
Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

impl UnableToDecryptReason {
Expand Down
6 changes: 6 additions & 0 deletions crates/matrix-sdk-crypto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ pub enum MegolmError {
/// The nested value is the sender's current verification level.
#[error("decryption failed because trust requirement not satisfied: {0}")]
SenderIdentityNotTrusted(VerificationLevel),

/// The outer state key could not be verified against the inner encrypted
/// state key and type.
#[cfg(feature = "experimental-encrypted-state-events")]
#[error("decryption failed because the state key failed to validate")]
StateKeyVerificationFailed,
Comment on lines +137 to +141
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This could be split into a larger variety of cases:

  • MalformedStateKey - when the outer encrypted event state key is not formatted as str:str.
  • MismatchBodyType - when the decrypted ciphertext is not a state event but the outer event is.
  • EventTypeMismatch - when the outer event type does not match the inner.
  • StateKeyMismatch - when the outer state key does not match the inner.

These may not be worth implementing?

}

/// Decryption failed because of a mismatch between the identity keys of the
Expand Down
2 changes: 2 additions & 0 deletions crates/matrix-sdk-crypto/src/gossiping/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,8 @@ mod tests {
EncryptedEvent {
sender: sender.to_owned(),
event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
#[cfg(feature = "experimental-encrypted-state-events")]
state_key: None,
content,
origin_server_ts: ruma::MilliSecondsSinceUnixEpoch::now(),
unsigned: Default::default(),
Expand Down
123 changes: 121 additions & 2 deletions crates/matrix-sdk-crypto/src/machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#[cfg(feature = "experimental-encrypted-state-events")]
use std::borrow::Borrow;
use std::{
collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
Expand All @@ -31,6 +33,8 @@ use matrix_sdk_common::{
locks::RwLock as StdRwLock,
BoxFuture,
};
#[cfg(feature = "experimental-encrypted-state-events")]
use ruma::events::{AnyStateEvent, AnyStateEventContent, StateEventContent};
use ruma::{
api::client::{
dehydrated_device::DehydratedDeviceData,
Expand Down Expand Up @@ -1102,6 +1106,66 @@ 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.
#[cfg(feature = "experimental-encrypted-state-events")]
pub async fn encrypt_state_event<C, K>(
&self,
room_id: &RoomId,
content: C,
state_key: K,
) -> MegolmResult<Raw<RoomEncryptedEventContent>>
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.
///
/// * `event_type` - The type of the event.
///
/// * `state_key` - The associated state key of the event.
///
/// * `content` - The plaintext content of the event that should be
/// encrypted as a raw JSON value.
#[cfg(feature = "experimental-encrypted-state-events")]
pub async fn encrypt_state_event_raw(
&self,
room_id: &RoomId,
event_type: &str,
state_key: &str,
content: &Raw<AnyStateEventContent>,
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
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 Expand Up @@ -2197,9 +2261,62 @@ impl OlmMachine {
.await;
}

let event = serde_json::from_value::<Raw<AnyTimelineEvent>>(decrypted_event.into())?;
let decrypted_event =
serde_json::from_value::<Raw<AnyTimelineEvent>>(decrypted_event.into())?;

#[cfg(feature = "experimental-encrypted-state-events")]
self.verify_packed_state_key(&event, &decrypted_event)?;

Ok(DecryptedRoomEvent { event: decrypted_event, encryption_info, unsigned_encryption_info })
}

/// If the passed event is a state event, verify its outer packed state key
/// matches the inner state key once unpacked.
///
/// * `original` - The original encrypted event received over the wire.
/// * `decrypted` - The decrypted event.
///
/// # Errors
///
/// Returns an error if
///
/// * The original event's state key failed to unpack;
/// * The decrypted event could not be deserialised;
/// * The unpacked event type does not match the type of the decrypted
/// event;
/// * The unpacked event state key does not match the state key of the
/// decrypted event.
#[cfg(feature = "experimental-encrypted-state-events")]
pub fn verify_packed_state_key(
&self,
original: &EncryptedEvent,
decrypted: &Raw<AnyTimelineEvent>,
) -> MegolmResult<()> {
// We only need to verify state events.
let Some(raw_state_key) = &original.state_key else { return Ok(()) };

// Unpack event type and state key from the raw state key.
let (outer_event_type, outer_state_key) =
raw_state_key.split_once(":").ok_or(MegolmError::StateKeyVerificationFailed)?;

// Deserialize the decrypted event.
let AnyTimelineEvent::State(inner) =
decrypted.deserialize().map_err(MegolmError::JsonError)?
else {
return Err(MegolmError::StateKeyVerificationFailed);
};

// Check event types match, discard if not.
let inner_event_type = inner.event_type().to_string();
if outer_event_type != inner_event_type {
return Err(MegolmError::StateKeyVerificationFailed);
}

Ok(DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info })
// Check state keys match, discard if not.
if outer_state_key != inner.state_key() {
return Err(MegolmError::StateKeyVerificationFailed);
}
Ok(())
}

/// Try to decrypt the events bundled in the `unsigned` object of the given
Expand Down Expand Up @@ -2970,6 +3087,8 @@ fn megolm_error_to_utd_info(
JsonError(_) => UnableToDecryptReason::PayloadDeserializationFailure,
MismatchedIdentityKeys(_) => UnableToDecryptReason::MismatchedIdentityKeys,
SenderIdentityNotTrusted(level) => UnableToDecryptReason::SenderIdentityNotTrusted(level),
#[cfg(feature = "experimental-encrypted-state-events")]
StateKeyVerificationFailed => UnableToDecryptReason::StateKeyVerificationFailed,

// Pass through crypto store errors, which indicate a problem with our
// application, rather than a UTD.
Expand Down
104 changes: 104 additions & 0 deletions crates/matrix-sdk-crypto/src/machine/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ use matrix_sdk_common::{
executor::spawn,
};
use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json};
#[cfg(feature = "experimental-encrypted-state-events")]
use ruma::events::{
room::topic::{OriginalRoomTopicEvent, RoomTopicEventContent},
StateEvent,
};
use ruma::{
api::client::{
keys::{get_keys, upload_keys},
Expand Down Expand Up @@ -727,6 +732,105 @@ async fn test_megolm_encryption() {
}
}

#[cfg(feature = "experimental-encrypted-state-events")]
#[async_test]
async fn test_megolm_state_encryption() {
use ruma::events::{AnyStateEvent, EmptyStateKey};

let (alice, bob) =
get_machine_pair_with_setup_sessions_test_helper(alice_id(), user_id(), false).await;
let room_id = room_id!("!test:example.org");

let to_device_requests = alice
.share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default())
.await
.unwrap();

let event = ToDeviceEvent::new(
alice.user_id().to_owned(),
to_device_requests_to_content(to_device_requests),
);

let mut room_keys_received_stream = Box::pin(bob.store().room_keys_received_stream());

let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

let group_session = bob
.store()
.with_transaction(|mut tr| async {
let res = bob
.decrypt_to_device_event(
&mut tr,
&event,
&mut Changes::default(),
&decryption_settings,
)
.await?;
Ok((tr, res))
})
.await
.unwrap()
.inbound_group_session
.unwrap();
let sessions = std::slice::from_ref(&group_session);
bob.store().save_inbound_group_sessions(sessions).await.unwrap();

// when we decrypt the room key, the
// inbound_group_session_streamroom_keys_received_stream should tell us
// about it.
let room_keys = room_keys_received_stream
.next()
.now_or_never()
.flatten()
.expect("We should have received an update of room key infos")
.unwrap();
assert_eq!(room_keys.len(), 1);
assert_eq!(room_keys[0].session_id, group_session.session_id());

let plaintext = "It is a secret to everybody";

let content = RoomTopicEventContent::new(plaintext.to_owned());

let encrypted_content =
alice.encrypt_state_event(room_id, content, EmptyStateKey).await.unwrap();

let event = json!({
"event_id": "$xxxxx:example.org",
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
"sender": alice.user_id(),
"type": "m.room.encrypted",
"state_key": "m.room.topic:",
"content": encrypted_content,
});

let event = json_convert(&event).unwrap();

let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

let decryption_result =
bob.try_decrypt_room_event(&event, room_id, &decryption_settings).await.unwrap();
assert_let!(RoomEventDecryptionResult::Decrypted(decrypted_event) = decryption_result);
let decrypted_event = decrypted_event.event.deserialize().unwrap();

if let AnyTimelineEvent::State(AnyStateEvent::RoomTopic(StateEvent::Original(
OriginalRoomTopicEvent { sender, content, .. },
))) = decrypted_event
{
assert_eq!(&sender, alice.user_id());
assert_eq!(&content.topic, plaintext);
} else {
panic!("Decrypted room event has the wrong type");
}

// Just decrypting the event should *not* cause an update on the
// inbound_group_session_stream.
if let Some(igs) = room_keys_received_stream.next().now_or_never() {
panic!("Session stream unexpectedly returned update: {igs:?}");
}
}

#[async_test]
async fn test_withheld_unverified() {
let (alice, bob) =
Expand Down
Loading
Loading