Skip to content

Commit 84ebbd9

Browse files
committed
feat: Add naive state key verification to OlmMachine
Modifies `OlmMachine::decrypt_room_event_inner` to call a new method `OlmMachine::verify_packed_state_key` which, if the event is a state event, verifies that the original event's state key, when unpacked, matches the state key and event type in the decrypted event content. Introduces MegolmError::StateKeyVerificationFailed and UnableToDecryptReason::StateKeyVerificationFailed which are thrown when the verification fails. Signed-off-by: Skye Elliot <[email protected]>
1 parent 756d507 commit 84ebbd9

File tree

6 files changed

+181
-9
lines changed

6 files changed

+181
-9
lines changed

crates/matrix-sdk-common/src/deserialized_responses.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,11 @@ pub enum UnableToDecryptReason {
998998
/// cross-signing identity did not satisfy the requested
999999
/// `TrustRequirement`.
10001000
SenderIdentityNotTrusted(VerificationLevel),
1001+
1002+
/// The outer state key could not be verified against the inner encrypted
1003+
/// state key and type.
1004+
#[cfg(feature = "experimental-encrypted-state-events")]
1005+
StateKeyVerificationFailed,
10011006
}
10021007

10031008
impl UnableToDecryptReason {

crates/matrix-sdk-crypto/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ pub enum MegolmError {
133133
/// The nested value is the sender's current verification level.
134134
#[error("decryption failed because trust requirement not satisfied: {0}")]
135135
SenderIdentityNotTrusted(VerificationLevel),
136+
137+
/// The outer state key could not be verified against the inner encrypted
138+
/// state key and type.
139+
#[cfg(feature = "experimental-encrypted-state-events")]
140+
#[error("decryption failed because the state key failed to validate")]
141+
StateKeyVerificationFailed,
136142
}
137143

138144
/// Decryption failed because of a mismatch between the identity keys of the

crates/matrix-sdk-crypto/src/gossiping/machine.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,8 @@ mod tests {
13641364
EncryptedEvent {
13651365
sender: sender.to_owned(),
13661366
event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
1367+
#[cfg(feature = "experimental-encrypted-state-events")]
1368+
state_key: None,
13671369
content,
13681370
origin_server_ts: ruma::MilliSecondsSinceUnixEpoch::now(),
13691371
unsigned: Default::default(),

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

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2261,9 +2261,72 @@ impl OlmMachine {
22612261
.await;
22622262
}
22632263

2264-
let event = serde_json::from_value::<Raw<AnyTimelineEvent>>(decrypted_event.into())?;
2264+
let decrypted_event =
2265+
serde_json::from_value::<Raw<AnyTimelineEvent>>(decrypted_event.into())?;
22652266

2266-
Ok(DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info })
2267+
#[cfg(feature = "experimental-encrypted-state-events")]
2268+
self.verify_packed_state_key(&event, &decrypted_event)?;
2269+
2270+
Ok(DecryptedRoomEvent { event: decrypted_event, encryption_info, unsigned_encryption_info })
2271+
}
2272+
2273+
/// If the passed event is a state event, verify its outer packed state key
2274+
/// matches the inner state key once unpacked.
2275+
///
2276+
/// * `original` - The original encrypted event received over the wire.
2277+
/// * `decrypted` - The decrypted event.
2278+
///
2279+
/// # Errors
2280+
///
2281+
/// Returns an error if any of the following are true:
2282+
///
2283+
/// * The original event's state key failed to unpack;
2284+
/// * The decrypted event could not be deserialised;
2285+
/// * The unpacked event type does not match the type of the decrypted
2286+
/// event;
2287+
/// * The unpacked event state key does not match the state key of the
2288+
/// decrypted event.
2289+
#[cfg(feature = "experimental-encrypted-state-events")]
2290+
fn verify_packed_state_key(
2291+
&self,
2292+
original: &EncryptedEvent,
2293+
decrypted: &Raw<AnyTimelineEvent>,
2294+
) -> MegolmResult<()> {
2295+
use serde::Deserialize;
2296+
2297+
// We only need to verify state events.
2298+
let Some(raw_state_key) = &original.state_key else { return Ok(()) };
2299+
2300+
// Unpack event type and state key from the raw state key.
2301+
let (outer_event_type, outer_state_key) =
2302+
raw_state_key.split_once(":").ok_or(MegolmError::StateKeyVerificationFailed)?;
2303+
2304+
// Helper for deserializing.
2305+
#[derive(Deserialize)]
2306+
struct PayloadDeserializationHelper {
2307+
state_key: String,
2308+
#[serde(rename = "type")]
2309+
event_type: String,
2310+
}
2311+
2312+
// Deserialize the decrypted event.
2313+
let PayloadDeserializationHelper {
2314+
state_key: inner_state_key,
2315+
event_type: inner_event_type,
2316+
} = decrypted
2317+
.deserialize_as_unchecked()
2318+
.map_err(|_| MegolmError::StateKeyVerificationFailed)?;
2319+
2320+
// Check event types match, discard if not.
2321+
if outer_event_type != inner_event_type {
2322+
return Err(MegolmError::StateKeyVerificationFailed);
2323+
}
2324+
2325+
// Check state keys match, discard if not.
2326+
if outer_state_key != inner_state_key {
2327+
return Err(MegolmError::StateKeyVerificationFailed);
2328+
}
2329+
Ok(())
22672330
}
22682331

22692332
/// Try to decrypt the events bundled in the `unsigned` object of the given
@@ -3034,6 +3097,8 @@ fn megolm_error_to_utd_info(
30343097
JsonError(_) => UnableToDecryptReason::PayloadDeserializationFailure,
30353098
MismatchedIdentityKeys(_) => UnableToDecryptReason::MismatchedIdentityKeys,
30363099
SenderIdentityNotTrusted(level) => UnableToDecryptReason::SenderIdentityNotTrusted(level),
3100+
#[cfg(feature = "experimental-encrypted-state-events")]
3101+
StateKeyVerificationFailed => UnableToDecryptReason::StateKeyVerificationFailed,
30373102

30383103
// Pass through crypto store errors, which indicate a problem with our
30393104
// application, rather than a UTD.

crates/matrix-sdk-crypto/src/machine/tests/mod.rs

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -733,13 +733,9 @@ async fn test_megolm_encryption() {
733733
}
734734

735735
#[cfg(feature = "experimental-encrypted-state-events")]
736-
#[async_test]
737-
async fn test_megolm_state_encryption() {
738-
use ruma::events::{AnyStateEvent, EmptyStateKey};
739-
736+
async fn megolm_encryption_setup_helper(room_id: &RoomId) -> (OlmMachine, OlmMachine) {
740737
let (alice, bob) =
741738
get_machine_pair_with_setup_sessions_test_helper(alice_id(), user_id(), false).await;
742-
let room_id = room_id!("!test:example.org");
743739

744740
let to_device_requests = alice
745741
.share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default())
@@ -774,10 +770,19 @@ async fn test_megolm_state_encryption() {
774770
let sessions = std::slice::from_ref(&group_session);
775771
bob.store().save_inbound_group_sessions(sessions).await.unwrap();
776772

777-
let plaintext = "It is a secret to everybody";
773+
(alice, bob)
774+
}
778775

779-
let content = RoomTopicEventContent::new(plaintext.to_owned());
776+
#[cfg(feature = "experimental-encrypted-state-events")]
777+
#[async_test]
778+
async fn test_megolm_state_encryption() {
779+
use ruma::events::{AnyStateEvent, EmptyStateKey};
780780

781+
let room_id = room_id!("!test:example.org");
782+
let (alice, bob) = megolm_encryption_setup_helper(room_id).await;
783+
784+
let plaintext = "It is a secret to everybody";
785+
let content = RoomTopicEventContent::new(plaintext.to_owned());
781786
let encrypted_content =
782787
alice.encrypt_state_event(room_id, content, EmptyStateKey).await.unwrap();
783788

@@ -786,6 +791,7 @@ async fn test_megolm_state_encryption() {
786791
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
787792
"sender": alice.user_id(),
788793
"type": "m.room.encrypted",
794+
"state_key": "m.room.topic:",
789795
"content": encrypted_content,
790796
});
791797

@@ -796,7 +802,9 @@ async fn test_megolm_state_encryption() {
796802

797803
let decryption_result =
798804
bob.try_decrypt_room_event(&event, room_id, &decryption_settings).await.unwrap();
805+
799806
assert_let!(RoomEventDecryptionResult::Decrypted(decrypted_event) = decryption_result);
807+
800808
let decrypted_event = decrypted_event.event.deserialize().unwrap();
801809

802810
if let AnyTimelineEvent::State(AnyStateEvent::RoomTopic(StateEvent::Original(
@@ -810,6 +818,88 @@ async fn test_megolm_state_encryption() {
810818
}
811819
}
812820

821+
#[cfg(feature = "experimental-encrypted-state-events")]
822+
#[async_test]
823+
async fn test_megolm_state_encryption_bad_type() {
824+
use ruma::events::EmptyStateKey;
825+
826+
let room_id = room_id!("!test:example.org");
827+
let (alice, bob) = megolm_encryption_setup_helper(room_id).await;
828+
829+
let plaintext = "It is a secret to everybody";
830+
let content = RoomTopicEventContent::new(plaintext.to_owned());
831+
let encrypted_content =
832+
alice.encrypt_state_event(room_id, content, EmptyStateKey).await.unwrap();
833+
834+
// Malformed events
835+
let bad_type_event = json!({
836+
"event_id": "$xxxxx:example.org",
837+
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
838+
"sender": alice.user_id(),
839+
"type": "m.room.encrypted",
840+
"state_key": "m.room.malformed:",
841+
"content": encrypted_content,
842+
});
843+
844+
let bad_type_event = json_convert(&bad_type_event).unwrap();
845+
846+
let decryption_settings =
847+
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
848+
849+
let bad_type_decryption_result =
850+
bob.try_decrypt_room_event(&bad_type_event, room_id, &decryption_settings).await.unwrap();
851+
852+
assert_matches!(
853+
bad_type_decryption_result,
854+
RoomEventDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
855+
reason: UnableToDecryptReason::StateKeyVerificationFailed,
856+
..
857+
})
858+
);
859+
}
860+
861+
#[cfg(feature = "experimental-encrypted-state-events")]
862+
#[async_test]
863+
async fn test_megolm_state_encryption_bad_state_key() {
864+
use ruma::events::EmptyStateKey;
865+
866+
let room_id = room_id!("!test:example.org");
867+
let (alice, bob) = megolm_encryption_setup_helper(room_id).await;
868+
869+
let plaintext = "It is a secret to everybody";
870+
let content = RoomTopicEventContent::new(plaintext.to_owned());
871+
let encrypted_content =
872+
alice.encrypt_state_event(room_id, content, EmptyStateKey).await.unwrap();
873+
874+
let bad_state_key_event = json!({
875+
"event_id": "$xxxxx:example.org",
876+
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
877+
"sender": alice.user_id(),
878+
"type": "m.room.encrypted",
879+
"state_key": "m.room.malformed:",
880+
"content": encrypted_content,
881+
});
882+
883+
let bad_state_key_event = json_convert(&bad_state_key_event).unwrap();
884+
885+
let decryption_settings =
886+
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
887+
888+
let bad_state_key_decryption_result = bob
889+
.try_decrypt_room_event(&bad_state_key_event, room_id, &decryption_settings)
890+
.await
891+
.unwrap();
892+
893+
// Require malformed events fail verification
894+
assert_matches!(
895+
bad_state_key_decryption_result,
896+
RoomEventDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
897+
reason: UnableToDecryptReason::StateKeyVerificationFailed,
898+
..
899+
})
900+
);
901+
}
902+
813903
#[async_test]
814904
async fn test_withheld_unverified() {
815905
let (alice, bob) =

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ where
3636
/// The globally unique identifier for this event.
3737
pub event_id: OwnedEventId,
3838

39+
/// Present if and only if this event is a state event.
40+
#[cfg(feature = "experimental-encrypted-state-events")]
41+
pub state_key: Option<String>,
42+
3943
/// The body of this event, as created by the client which sent it.
4044
pub content: C,
4145

0 commit comments

Comments
 (0)